Bug 960771 - Uplift Add-on SDK
authorErik Vold <evold@mozilla.com>
Thu, 16 Jan 2014 17:29:40 -0800
changeset 163990 cf3f073c8b4bc54f4c0ecd94f4b9cecc0290199b
parent 163989 356ef7c535f236a5c29d6911d69ec945d0a9c45f
child 163991 14a4f50a46811dfa1f70298407cac23ae1012c14
push id26022
push userryanvm@gmail.com
push dateFri, 17 Jan 2014 19:56:22 +0000
treeherdermozilla-central@fad7172d4542 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs960771
milestone29.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 960771 - Uplift Add-on SDK
addon-sdk/source/lib/sdk/content/content.js
addon-sdk/source/lib/sdk/content/sandbox.js
addon-sdk/source/lib/sdk/content/symbiont.js
addon-sdk/source/lib/sdk/content/utils.js
addon-sdk/source/lib/sdk/content/worker.js
addon-sdk/source/lib/sdk/context-menu.js
addon-sdk/source/lib/sdk/deprecated/api-utils.js
addon-sdk/source/lib/sdk/deprecated/app-strings.js
addon-sdk/source/lib/sdk/deprecated/memory.js
addon-sdk/source/lib/sdk/deprecated/observer-service.js
addon-sdk/source/lib/sdk/deprecated/symbiont.js
addon-sdk/source/lib/sdk/deprecated/traits-worker.js
addon-sdk/source/lib/sdk/event/core.js
addon-sdk/source/lib/sdk/event/utils.js
addon-sdk/source/lib/sdk/l10n/prefs.js
addon-sdk/source/lib/sdk/page-mod.js
addon-sdk/source/lib/sdk/page-worker.js
addon-sdk/source/lib/sdk/panel.js
addon-sdk/source/lib/sdk/simple-prefs.js
addon-sdk/source/lib/sdk/test/harness.js
addon-sdk/source/lib/sdk/test/memory.js
addon-sdk/source/lib/sdk/ui/button/action.js
addon-sdk/source/lib/sdk/ui/button/toggle.js
addon-sdk/source/lib/sdk/ui/button/view.js
addon-sdk/source/lib/sdk/ui/frame.js
addon-sdk/source/lib/sdk/ui/sidebar.js
addon-sdk/source/lib/sdk/ui/toolbar.js
addon-sdk/source/lib/sdk/window/utils.js
addon-sdk/source/lib/sdk/worker/utils.js
addon-sdk/source/lib/toolkit/loader.js
addon-sdk/source/mapping.json
addon-sdk/source/test/addons/layout-change/main.js
addon-sdk/source/test/addons/symbiont/main.js
addon-sdk/source/test/fixtures/loader/missing-twice/file.json
addon-sdk/source/test/fixtures/loader/missing-twice/main.js
addon-sdk/source/test/tabs/test-firefox-tabs.js
addon-sdk/source/test/test-addon-installer.js
addon-sdk/source/test/test-api-utils.js
addon-sdk/source/test/test-app-strings.js
addon-sdk/source/test/test-content-symbiont.js
addon-sdk/source/test/test-content-worker.js
addon-sdk/source/test/test-event-core.js
addon-sdk/source/test/test-event-utils.js
addon-sdk/source/test/test-loader.js
addon-sdk/source/test/test-memory.js
addon-sdk/source/test/test-observer-service.js
addon-sdk/source/test/test-simple-prefs.js
addon-sdk/source/test/test-test-memory.js
addon-sdk/source/test/test-traceback.js
addon-sdk/source/test/test-window-utils2.js
addon-sdk/source/test/test-windows-common.js
addon-sdk/source/test/windows/test-firefox-windows.js
--- a/addon-sdk/source/lib/sdk/content/content.js
+++ b/addon-sdk/source/lib/sdk/content/content.js
@@ -3,11 +3,11 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
 exports.Loader = require('./loader').Loader;
-exports.Symbiont = require('./symbiont').Symbiont;
+exports.Symbiont = require('../deprecated/symbiont').Symbiont;
 exports.Worker = require('./worker').Worker;
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/content/sandbox.js
@@ -0,0 +1,404 @@
+/* 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';
+
+module.metadata = {
+  'stability': 'unstable'
+};
+
+const { Class } = require('../core/heritage');
+const { EventTarget } = require('../event/target');
+const { on, off, emit } = require('../event/core');
+const {
+  requiresAddonGlobal,
+  attach, detach, destroy
+} = require('./utils');
+const { delay: async } = require('../lang/functional');
+const { Ci, Cu, Cc } = require('chrome');
+const timer = require('../timers');
+const { URL } = require('../url');
+const { sandbox, evaluate, load } = require('../loader/sandbox');
+const { merge } = require('../util/object');
+const xulApp = require('../system/xul-app');
+const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
+                                              '17.0a2', '*');
+const { getTabForContentWindow } = require('../tabs/utils');
+
+// WeakMap of sandboxes so we can access private values
+const sandboxes = new WeakMap();
+
+/* Trick the linker in order to ensure shipping these files in the XPI.
+  require('./content-worker.js');
+  Then, retrieve URL of these files in the XPI:
+*/
+let prefix = module.uri.split('sandbox.js')[0];
+const CONTENT_WORKER_URL = prefix + 'content-worker.js';
+
+// Fetch additional list of domains to authorize access to for each content
+// script. It is stored in manifest `metadata` field which contains
+// package.json data. This list is originaly defined by authors in
+// `permissions` attribute of their package.json addon file.
+const permissions = require('@loader/options').metadata['permissions'] || {};
+const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
+
+const JS_VERSION = '1.8';
+
+const WorkerSandbox = Class({
+
+  implements: [
+    EventTarget
+  ],
+  
+  /**
+   * Emit a message to the worker content sandbox
+   */
+  emit: function emit(...args) {
+    // Ensure having an asynchronous behavior
+    let self = this;
+    async(function () {
+      emitToContent(self, JSON.stringify(args, replacer));
+    });
+  },
+
+  /**
+   * Synchronous version of `emit`.
+   * /!\ Should only be used when it is strictly mandatory /!\
+   *     Doesn't ensure passing only JSON values.
+   *     Mainly used by context-menu in order to avoid breaking it.
+   */
+  emitSync: function emitSync(...args) {
+    return emitToContent(this, args);
+  },
+
+  /**
+   * Tells if content script has at least one listener registered for one event,
+   * through `self.on('xxx', ...)`.
+   * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
+   */
+  hasListenerFor: function hasListenerFor(name) {
+    return modelFor(this).hasListenerFor(name);
+  },
+
+  /**
+   * Configures sandbox and loads content scripts into it.
+   * @param {Worker} worker
+   *    content worker
+   */
+  initialize: function WorkerSandbox(worker, window) {
+    let model = {};
+    sandboxes.set(this, model);
+    model.worker = worker;
+    // We receive a wrapped window, that may be an xraywrapper if it's content
+    let proto = window;
+
+    // TODO necessary?
+    // Ensure that `emit` has always the right `this`
+    this.emit = this.emit.bind(this);
+    this.emitSync = this.emitSync.bind(this);
+
+    // Eventually use expanded principal sandbox feature, if some are given.
+    //
+    // But prevent it when the Worker isn't used for a content script but for
+    // injecting `addon` object into a Panel, Widget, ... scope.
+    // That's because:
+    // 1/ It is useless to use multiple domains as the worker is only used
+    // to communicate with the addon,
+    // 2/ By using it it would prevent the document to have access to any JS
+    // value of the worker. As JS values coming from multiple domain principals
+    // can't be accessed by 'mono-principals' (principal with only one domain).
+    // Even if this principal is for a domain that is specified in the multiple
+    // domain principal.
+    let principals = window;
+    let wantGlobalProperties = [];
+    if (EXPANDED_PRINCIPALS.length > 0 && !requiresAddonGlobal(worker)) {
+      principals = EXPANDED_PRINCIPALS.concat(window);
+      // We have to replace XHR constructor of the content document
+      // with a custom cross origin one, automagically added by platform code:
+      delete proto.XMLHttpRequest;
+      wantGlobalProperties.push('XMLHttpRequest');
+    }
+
+    // Instantiate trusted code in another Sandbox in order to prevent content
+    // script from messing with standard classes used by proxy and API code.
+    let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
+    apiSandbox.console = console;
+
+    // Create the sandbox and bind it to window in order for content scripts to
+    // have access to all standard globals (window, document, ...)
+    let content = sandbox(principals, {
+      sandboxPrototype: proto,
+      wantXrays: true,
+      wantGlobalProperties: wantGlobalProperties,
+      sameZoneAs: window,
+      metadata: { SDKContentScript: true }
+    });
+    model.sandbox = content;
+    
+    // We have to ensure that window.top and window.parent are the exact same
+    // object than window object, i.e. the sandbox global object. But not
+    // always, in case of iframes, top and parent are another window object.
+    let top = window.top === window ? content : content.top;
+    let parent = window.parent === window ? content : content.parent;
+    merge(content, {
+      // We need 'this === window === top' to be true in toplevel scope:
+      get window() content,
+      get top() top,
+      get parent() parent,
+      // Use the Greasemonkey naming convention to provide access to the
+      // unwrapped window object so the content script can access document
+      // JavaScript values.
+      // NOTE: this functionality is experimental and may change or go away
+      // at any time!
+      get unsafeWindow() window.wrappedJSObject
+    });
+
+    // Load trusted code that will inject content script API.
+    // We need to expose JS objects defined in same principal in order to
+    // avoid having any kind of wrapper.
+    load(apiSandbox, CONTENT_WORKER_URL);
+
+    // prepare a clean `self.options`
+    let options = 'contentScriptOptions' in worker ?
+      JSON.stringify(worker.contentScriptOptions) :
+      undefined;
+
+    // Then call `inject` method and communicate with this script
+    // by trading two methods that allow to send events to the other side:
+    //   - `onEvent` called by content script
+    //   - `result.emitToContent` called by addon script
+    // Bug 758203: We have to explicitely define `__exposedProps__` in order
+    // to allow access to these chrome object attributes from this sandbox with
+    // content priviledges
+    // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
+    let onEvent = onContentEvent.bind(null, this);
+    // `ContentWorker` is defined in CONTENT_WORKER_URL file
+    let chromeAPI = createChromeAPI();
+    let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
+
+    // Merge `emitToContent` and `hasListenerFor` into our private
+    // model of the WorkerSandbox so we can communicate with content
+    // script
+    merge(model, result);
+
+    // Handle messages send by this script:
+    setListeners(this);
+
+    // Inject `addon` global into target document if document is trusted,
+    // `addon` in document is equivalent to `self` in content script.
+    if (requiresAddonGlobal(worker)) {
+      Object.defineProperty(getUnsafeWindow(window), 'addon', {
+          value: content.self
+        }
+      );
+    }
+
+    // Inject our `console` into target document if worker doesn't have a tab
+    // (e.g Panel, PageWorker, Widget).
+    // `worker.tab` can't be used because bug 804935.
+    if (!getTabForContentWindow(window)) {
+      let win = getUnsafeWindow(window);
+
+      // export our chrome console to content window, using the same approach
+      // of `ConsoleAPI`:
+      // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
+      //
+      // and described here:
+      // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
+      let con = Cu.createObjectIn(win);
+
+      let genPropDesc = function genPropDesc(fun) {
+        return { enumerable: true, configurable: true, writable: true,
+          value: console[fun] };
+      }
+
+      const properties = {
+        log: genPropDesc('log'),
+        info: genPropDesc('info'),
+        warn: genPropDesc('warn'),
+        error: genPropDesc('error'),
+        debug: genPropDesc('debug'),
+        trace: genPropDesc('trace'),
+        dir: genPropDesc('dir'),
+        group: genPropDesc('group'),
+        groupCollapsed: genPropDesc('groupCollapsed'),
+        groupEnd: genPropDesc('groupEnd'),
+        time: genPropDesc('time'),
+        timeEnd: genPropDesc('timeEnd'),
+        profile: genPropDesc('profile'),
+        profileEnd: genPropDesc('profileEnd'),
+       __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
+                            value: function() {} }
+      };
+
+      Object.defineProperties(con, properties);
+      Cu.makeObjectPropsNormal(con);
+
+      win.console = con;
+    };
+
+    // The order of `contentScriptFile` and `contentScript` evaluation is
+    // intentional, so programs can load libraries like jQuery from script URLs
+    // and use them in scripts.
+    let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
+          : null,
+        contentScript = ('contentScript' in worker) ? worker.contentScript : null;
+
+    if (contentScriptFile)
+      importScripts.apply(null, [this].concat(contentScriptFile));
+    if (contentScript) {
+      evaluateIn(
+        this,
+        Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
+      );
+    }
+  },
+  destroy: function destroy() {
+    this.emitSync('detach');
+    let model = modelFor(this);
+    model.sandbox = null
+    model.worker = null;
+  },
+
+});
+
+exports.WorkerSandbox = WorkerSandbox;
+
+/**
+ * Imports scripts to the sandbox by reading files under urls and
+ * evaluating its source. If exception occurs during evaluation
+ * `'error'` event is emitted on the worker.
+ * This is actually an analog to the `importScript` method in web
+ * workers but in our case it's not exposed even though content
+ * scripts may be able to do it synchronously since IO operation
+ * takes place in the UI process.
+ */
+function importScripts (workerSandbox, ...urls) {
+  let { worker, sandbox } = modelFor(workerSandbox);
+  for (let i in urls) {
+    let contentScriptFile = urls[i];
+    try {
+      let uri = URL(contentScriptFile);
+      if (uri.scheme === 'resource')
+        load(sandbox, String(uri));
+      else
+        throw Error('Unsupported `contentScriptFile` url: ' + String(uri));
+    }
+    catch(e) {
+      emit(worker, 'error', e);
+    }
+  }
+}
+
+function setListeners (workerSandbox) {
+  let { worker } = modelFor(workerSandbox);
+  // console.xxx calls
+  workerSandbox.on('console', function consoleListener (kind, ...args) {
+    console[kind].apply(console, args);
+  });
+
+  // self.postMessage calls
+  workerSandbox.on('message', function postMessage(data) {
+    // destroyed?
+    if (worker)
+      emit(worker, 'message', data);
+  });
+
+  // self.port.emit calls
+  workerSandbox.on('event', function portEmit (...eventArgs) {
+    // If not destroyed, emit event information to worker
+    // `eventArgs` has the event name as first element,
+    // and remaining elements are additional arguments to pass
+    if (worker)
+      emit.apply(null, [worker.port].concat(eventArgs));
+  });
+
+  // unwrap, recreate and propagate async Errors thrown from content-script
+  workerSandbox.on('error', function onError({instanceOfError, value}) {
+    if (worker) {
+      let error = value;
+      if (instanceOfError) {
+        error = new Error(value.message, value.fileName, value.lineNumber);
+        error.stack = value.stack;
+        error.name = value.name;
+      }
+      emit(worker, 'error', error);
+    }
+  });
+}
+
+/**
+ * Evaluates code in the sandbox.
+ * @param {String} code
+ *    JavaScript source to evaluate.
+ * @param {String} [filename='javascript:' + code]
+ *    Name of the file
+ */
+function evaluateIn (workerSandbox, code, filename) {
+  let { worker, sandbox } = modelFor(workerSandbox);
+  try {
+    evaluate(sandbox, code, filename || 'javascript:' + code);
+  }
+  catch(e) {
+    emit(worker, 'error', e);
+  }
+}
+
+/**
+ * Method called by the worker sandbox when it needs to send a message
+ */
+function onContentEvent (workerSandbox, args) {
+  // As `emit`, we ensure having an asynchronous behavior
+  async(function () {
+    // We emit event to chrome/addon listeners
+    emit.apply(null, [workerSandbox].concat(JSON.parse(args)));
+  });
+}
+
+
+function modelFor (workerSandbox) {
+  return sandboxes.get(workerSandbox);
+}
+
+/**
+ * JSON.stringify is buggy with cross-sandbox values,
+ * it may return '{}' on functions. Use a replacer to match them correctly.
+ */
+function replacer (k, v) {
+  return typeof v === 'function' ? undefined : v;
+}
+
+function getUnsafeWindow (win) {
+  return win.wrappedJSObject || win;
+}
+
+function emitToContent (workerSandbox, args) {
+  return modelFor(workerSandbox).emitToContent(args);
+}
+
+function createChromeAPI () {
+  return {
+    timers: {
+      setTimeout: timer.setTimeout,
+      setInterval: timer.setInterval,
+      clearTimeout: timer.clearTimeout,
+      clearInterval: timer.clearInterval,
+      __exposedProps__: {
+        setTimeout: 'r',
+        setInterval: 'r',
+        clearTimeout: 'r',
+        clearInterval: 'r'
+      },
+    },
+    sandbox: {
+      evaluate: evaluate,
+      __exposedProps__: {
+        evaluate: 'r'
+      }
+    },
+    __exposedProps__: {
+      timers: 'r',
+      sandbox: 'r'
+    }
+  };
+}
deleted file mode 100644
--- a/addon-sdk/source/lib/sdk/content/symbiont.js
+++ /dev/null
@@ -1,229 +0,0 @@
-/* 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";
-
-module.metadata = {
-  "stability": "unstable"
-};
-
-const { Worker } = require('./worker');
-const { Loader } = require('./loader');
-const hiddenFrames = require('../frame/hidden-frame');
-const observers = require('../deprecated/observer-service');
-const unload = require('../system/unload');
-const { getDocShell } = require("../frame/utils");
-const { ignoreWindow } = require('../private-browsing/utils');
-
-// Everything coming from add-on's xpi considered an asset.
-const assetsURI = require('../self').data.url().replace(/data\/$/, "");
-
-/**
- * This trait is layered on top of `Worker` and in contrast to symbiont
- * Worker constructor requires `content` option that represents content
- * that will be loaded in the provided frame, if frame is not provided
- * Worker will create hidden one.
- */
-const Symbiont = Worker.resolve({
-    constructor: '_initWorker',
-    destroy: '_workerDestroy'
-  }).compose(Loader, {
-  
-  /**
-   * The constructor requires all the options that are required by
-   * `require('content').Worker` with the difference that the `frame` option
-   * is optional. If `frame` is not provided, `contentURL` is expected.
-   * @param {Object} options
-   * @param {String} options.contentURL
-   *    URL of a content to load into `this._frame` and create worker for.
-   * @param {Element} [options.frame]
-   *    iframe element that is used to load `options.contentURL` into.
-   *    if frame is not provided hidden iframe will be created.
-   */
-  constructor: function Symbiont(options) {
-    options = options || {};
-
-    if ('contentURL' in options)
-        this.contentURL = options.contentURL;
-    if ('contentScriptWhen' in options)
-      this.contentScriptWhen = options.contentScriptWhen;
-    if ('contentScriptOptions' in options)
-      this.contentScriptOptions = options.contentScriptOptions;
-    if ('contentScriptFile' in options)
-      this.contentScriptFile = options.contentScriptFile;
-    if ('contentScript' in options)
-      this.contentScript = options.contentScript;
-    if ('allow' in options)
-      this.allow = options.allow;
-    if ('onError' in options)
-        this.on('error', options.onError);
-    if ('onMessage' in options)
-        this.on('message', options.onMessage);
-    if ('frame' in options) {
-      this._initFrame(options.frame);
-    }
-    else {
-      let self = this;
-      this._hiddenFrame = hiddenFrames.HiddenFrame({
-        onReady: function onFrame() {
-          self._initFrame(this.element);
-        },
-        onUnload: function onUnload() {
-          // Bug 751211: Remove reference to _frame when hidden frame is
-          // automatically removed on unload, otherwise we are going to face
-          // "dead object" exception
-          self.destroy();
-        }
-      });
-      hiddenFrames.add(this._hiddenFrame);
-    }
-
-    unload.ensure(this._public, "destroy");
-  },
-  
-  destroy: function destroy() {
-    this._workerDestroy();
-    this._unregisterListener();
-    this._frame = null;
-    if (this._hiddenFrame) {
-      hiddenFrames.remove(this._hiddenFrame);
-      this._hiddenFrame = null;
-    }
-  },
-  
-  /**
-   * XUL iframe or browser elements with attribute `type` being `content`.
-   * Used to create `ContentSymbiont` from.
-   * @type {nsIFrame|nsIBrowser}
-   */
-  _frame: null,
-  
-  /**
-   * Listener to the `'frameReady"` event (emitted when `iframe` is ready).
-   * Removes listener, sets right permissions to the frame and loads content.
-   */
-  _initFrame: function _initFrame(frame) {
-    if (this._loadListener)
-      this._unregisterListener();
-    
-    this._frame = frame;
-
-    if (getDocShell(frame)) {
-      this._reallyInitFrame(frame);
-    }
-    else {
-      if (this._waitForFrame) {
-        observers.remove('content-document-global-created', this._waitForFrame);
-      }
-      this._waitForFrame = this.__waitForFrame.bind(this, frame);
-      observers.add('content-document-global-created', this._waitForFrame);
-    }
-  },
-
-  __waitForFrame: function _waitForFrame(frame, win, topic) {
-    if (frame.contentWindow == win) {
-      observers.remove('content-document-global-created', this._waitForFrame);
-      delete this._waitForFrame;
-      this._reallyInitFrame(frame);
-    }
-  },
-
-  _reallyInitFrame: function _reallyInitFrame(frame) {
-    getDocShell(frame).allowJavascript = this.allow.script;
-    frame.setAttribute("src", this._contentURL);
-
-    // Inject `addon` object in document if we load a document from
-    // one of our addon folder and if no content script are defined. bug 612726
-    let isDataResource =
-      typeof this._contentURL == "string" &&
-      this._contentURL.indexOf(assetsURI) == 0;
-    let hasContentScript =
-      (Array.isArray(this.contentScript) ? this.contentScript.length > 0
-                                             : !!this.contentScript) ||
-      (Array.isArray(this.contentScriptFile) ? this.contentScriptFile.length > 0
-                                             : !!this.contentScriptFile);
-    // If we have to inject `addon` we have to do it before document
-    // script execution, so during `start`:
-    this._injectInDocument = isDataResource && !hasContentScript;
-    if (this._injectInDocument)
-      this.contentScriptWhen = "start";
-
-    if ((frame.contentDocument.readyState == "complete" ||
-        (frame.contentDocument.readyState == "interactive" &&
-         this.contentScriptWhen != 'end' )) &&
-        frame.contentDocument.location == this._contentURL) {
-      // In some cases src doesn't change and document is already ready
-      // (for ex: when the user moves a widget while customizing toolbars.)
-      this._onInit();
-      return;
-    }
-    
-    let self = this;
-    
-    if ('start' == this.contentScriptWhen) {
-      this._loadEvent = 'start';
-      observers.add('document-element-inserted', 
-        this._loadListener = function onStart(doc) {
-          let window = doc.defaultView;
-
-          if (ignoreWindow(window)) {
-            return;
-          }
-
-          if (window && window == frame.contentWindow) {
-            self._unregisterListener();
-            self._onInit();
-          }
-          
-        });
-      return;
-    }
-    
-    let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded';
-    let self = this;
-    this._loadEvent = eventName;
-    frame.addEventListener(eventName, 
-      this._loadListener = function _onReady(event) {
-      
-        if (event.target != frame.contentDocument)
-          return;
-        self._unregisterListener();
-        
-        self._onInit();
-        
-      }, true);
-    
-  },
-  
-  /**
-   * Unregister listener that watchs for document being ready to be injected.
-   * This listener is registered in `Symbiont._initFrame`.
-   */
-  _unregisterListener: function _unregisterListener() {
-    if (this._waitForFrame) {
-      observers.remove('content-document-global-created', this._waitForFrame);
-      delete this._waitForFrame;
-    }
-
-    if (!this._loadListener)
-      return;
-    if (this._loadEvent == "start") {
-      observers.remove('document-element-inserted', this._loadListener);
-    }
-    else {
-      this._frame.removeEventListener(this._loadEvent, this._loadListener,
-                                      true);
-    }
-    this._loadListener = null;
-  },
-  
-  /**
-   * Called by Symbiont itself when the frame is ready to load  
-   * content scripts according to contentScriptWhen. Overloaded by Panel. 
-   */
-  _onInit: function () {
-    this._initWorker({ window: this._frame.contentWindow });
-  }
-  
-});
-exports.Symbiont = Symbiont;
--- a/addon-sdk/source/lib/sdk/content/utils.js
+++ b/addon-sdk/source/lib/sdk/content/utils.js
@@ -1,41 +1,82 @@
 /* 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";
+'use strict';
 
 module.metadata = {
-  "stability": "unstable"
+  'stability': 'unstable'
 };
 
-let assetsURI = require("../self").data.url();
+let { merge } = require('../util/object');
+let assetsURI = require('../self').data.url();
 let isArray = Array.isArray;
+let method = require('method/core');
 
 function isAddonContent({ contentURL }) {
-  return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0;
+  return typeof(contentURL) === 'string' && contentURL.indexOf(assetsURI) === 0;
 }
 exports.isAddonContent = isAddonContent;
 
 function hasContentScript({ contentScript, contentScriptFile }) {
   return (isArray(contentScript) ? contentScript.length > 0 :
          !!contentScript) ||
          (isArray(contentScriptFile) ? contentScriptFile.length > 0 :
          !!contentScriptFile);
 }
 exports.hasContentScript = hasContentScript;
 
 function requiresAddonGlobal(model) {
-  return isAddonContent(model) && !hasContentScript(model);
+  return model.injectInDocument || (isAddonContent(model) && !hasContentScript(model));
 }
 exports.requiresAddonGlobal = requiresAddonGlobal;
 
 function getAttachEventType(model) {
   if (!model) return null;
   let when = model.contentScriptWhen;
-  return requiresAddonGlobal(model) ? "document-element-inserted" :
-         when === "start" ? "document-element-inserted" :
-         when === "end" ? "load" :
-         when === "ready" ? "DOMContentLoaded" :
+  return requiresAddonGlobal(model) ? 'document-element-inserted' :
+         when === 'start' ? 'document-element-inserted' :
+         when === 'end' ? 'load' :
+         when === 'ready' ? 'DOMContentLoaded' :
          null;
 }
 exports.getAttachEventType = getAttachEventType;
 
+let attach = method('worker-attach');
+exports.attach = attach;
+
+let detach = method('worker-detach');
+exports.detach = detach;
+
+let destroy = method('worker-destroy');
+exports.destroy = destroy;
+
+function WorkerHost (workerFor) {
+  // Define worker properties that just proxy to underlying worker
+  return ['postMessage', 'port', 'url', 'tab'].reduce(function(proto, name) {
+    // Use descriptor properties instead so we can call
+    // the worker function in the context of the worker so we
+    // don't have to create new functions with `fn.bind(worker)`
+    let descriptorProp = {
+      value: function (...args) {
+        let worker = workerFor(this);
+        return worker[name].apply(worker, args);
+      }
+    };
+    
+    let accessorProp = {
+      get: function () { return workerFor(this)[name]; },
+      set: function (value) { workerFor(this)[name] = value; }
+    };
+
+    Object.defineProperty(proto, name, merge({
+      enumerable: true,
+      configurable: false,
+    }, isDescriptor(name) ? descriptorProp : accessorProp));
+    return proto;
+  }, {});
+  
+  function isDescriptor (prop) {
+    return ~['postMessage'].indexOf(prop);
+  }
+}
+exports.WorkerHost = WorkerHost;
--- a/addon-sdk/source/lib/sdk/content/worker.js
+++ b/addon-sdk/source/lib/sdk/content/worker.js
@@ -2,650 +2,281 @@
  * 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";
 
 module.metadata = {
   "stability": "unstable"
 };
 
-const { Trait } = require('../deprecated/traits');
-const { EventEmitter, EventEmitterTrait } = require('../deprecated/events');
+const { Class } = require('../core/heritage');
+const { EventTarget } = require('../event/target');
+const { on, off, emit, setListeners } = require('../event/core');
+const {
+  attach, detach, destroy
+} = require('./utils');
+const { method } = require('../lang/functional');
 const { Ci, Cu, Cc } = require('chrome');
-const timer = require('../timers');
-const { URL } = require('../url');
 const unload = require('../system/unload');
-const observers = require('../deprecated/observer-service');
-const { Cortex } = require('../deprecated/cortex');
-const { sandbox, evaluate, load } = require("../loader/sandbox");
-const { merge } = require('../util/object');
-const xulApp = require("../system/xul-app");
-const { getInnerId } = require("../window/utils")
-const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
-                                              "17.0a2", "*");
+const events = require('../system/events');
+const { getInnerId } = require("../window/utils");
+const { WorkerSandbox } = require('./sandbox');
 const { getTabForWindow } = require('../tabs/helpers');
-const { getTabForContentWindow } = require('../tabs/utils');
-
-/* Trick the linker in order to ensure shipping these files in the XPI.
-  require('./content-worker.js');
-  Then, retrieve URL of these files in the XPI:
-*/
-let prefix = module.uri.split('worker.js')[0];
-const CONTENT_WORKER_URL = prefix + 'content-worker.js';
 
-// Fetch additional list of domains to authorize access to for each content
-// script. It is stored in manifest `metadata` field which contains
-// package.json data. This list is originaly defined by authors in
-// `permissions` attribute of their package.json addon file.
-const permissions = require('@loader/options').metadata['permissions'] || {};
-const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
+// A weak map of workers to hold private attributes that
+// should not be exposed
+const workers = new WeakMap();
 
-const JS_VERSION = '1.8';
+let modelFor = (worker) => workers.get(worker);
 
 const ERR_DESTROYED =
   "Couldn't find the worker to receive this message. " +
   "The script may not be initialized yet, or may already have been unloaded.";
 
 const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
                    "until it is visible again.";
 
 
-const WorkerSandbox = EventEmitter.compose({
-
-  /**
-   * Emit a message to the worker content sandbox
-   */
-  emit: function emit() {
-    // First ensure having a regular array
-    // (otherwise, `arguments` would be mapped to an object by `stringify`)
-    let array = Array.slice(arguments);
-    // JSON.stringify is buggy with cross-sandbox values,
-    // it may return "{}" on functions. Use a replacer to match them correctly.
-    function replacer(k, v) {
-      return typeof v === "function" ? undefined : v;
-    }
-    // Ensure having an asynchronous behavior
-    let self = this;
-    timer.setTimeout(function () {
-      self._emitToContent(JSON.stringify(array, replacer));
-    }, 0);
-  },
-
-  /**
-   * Synchronous version of `emit`.
-   * /!\ Should only be used when it is strictly mandatory /!\
-   *     Doesn't ensure passing only JSON values.
-   *     Mainly used by context-menu in order to avoid breaking it.
-   */
-  emitSync: function emitSync() {
-    let args = Array.slice(arguments);
-    return this._emitToContent(args);
-  },
-
-  /**
-   * Tells if content script has at least one listener registered for one event,
-   * through `self.on('xxx', ...)`.
-   * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
-   */
-  hasListenerFor: function hasListenerFor(name) {
-    return this._hasListenerFor(name);
-  },
-
-  /**
-   * Method called by the worker sandbox when it needs to send a message
-   */
-  _onContentEvent: function onContentEvent(args) {
-    // As `emit`, we ensure having an asynchronous behavior
-    let self = this;
-    timer.setTimeout(function () {
-      // We emit event to chrome/addon listeners
-      self._emit.apply(self, JSON.parse(args));
-    }, 0);
-  },
-
-  /**
-   * Configures sandbox and loads content scripts into it.
-   * @param {Worker} worker
-   *    content worker
-   */
-  constructor: function WorkerSandbox(worker) {
-    this._addonWorker = worker;
-
-    // Ensure that `emit` has always the right `this`
-    this.emit = this.emit.bind(this);
-    this.emitSync = this.emitSync.bind(this);
-
-    // We receive a wrapped window, that may be an xraywrapper if it's content
-    let window = worker._window;
-    let proto = window;
-
-    // Eventually use expanded principal sandbox feature, if some are given.
-    //
-    // But prevent it when the Worker isn't used for a content script but for
-    // injecting `addon` object into a Panel, Widget, ... scope.
-    // That's because:
-    // 1/ It is useless to use multiple domains as the worker is only used
-    // to communicate with the addon,
-    // 2/ By using it it would prevent the document to have access to any JS
-    // value of the worker. As JS values coming from multiple domain principals
-    // can't be accessed by "mono-principals" (principal with only one domain).
-    // Even if this principal is for a domain that is specified in the multiple
-    // domain principal.
-    let principals  = window;
-    let wantGlobalProperties = []
-    if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) {
-      principals = EXPANDED_PRINCIPALS.concat(window);
-      // We have to replace XHR constructor of the content document
-      // with a custom cross origin one, automagically added by platform code:
-      delete proto.XMLHttpRequest;
-      wantGlobalProperties.push("XMLHttpRequest");
-    }
-
-    // Instantiate trusted code in another Sandbox in order to prevent content
-    // script from messing with standard classes used by proxy and API code.
-    let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
-    apiSandbox.console = console;
-
-    // Create the sandbox and bind it to window in order for content scripts to
-    // have access to all standard globals (window, document, ...)
-    let content = this._sandbox = sandbox(principals, {
-      sandboxPrototype: proto,
-      wantXrays: true,
-      wantGlobalProperties: wantGlobalProperties,
-      sameZoneAs: window,
-      metadata: { SDKContentScript: true }
-    });
-    // We have to ensure that window.top and window.parent are the exact same
-    // object than window object, i.e. the sandbox global object. But not
-    // always, in case of iframes, top and parent are another window object.
-    let top = window.top === window ? content : content.top;
-    let parent = window.parent === window ? content : content.parent;
-    merge(content, {
-      // We need "this === window === top" to be true in toplevel scope:
-      get window() content,
-      get top() top,
-      get parent() parent,
-      // Use the Greasemonkey naming convention to provide access to the
-      // unwrapped window object so the content script can access document
-      // JavaScript values.
-      // NOTE: this functionality is experimental and may change or go away
-      // at any time!
-      get unsafeWindow() window.wrappedJSObject
-    });
-
-    // Load trusted code that will inject content script API.
-    // We need to expose JS objects defined in same principal in order to
-    // avoid having any kind of wrapper.
-    load(apiSandbox, CONTENT_WORKER_URL);
-
-    // prepare a clean `self.options`
-    let options = 'contentScriptOptions' in worker ?
-      JSON.stringify( worker.contentScriptOptions ) :
-      undefined;
-
-    // Then call `inject` method and communicate with this script
-    // by trading two methods that allow to send events to the other side:
-    //   - `onEvent` called by content script
-    //   - `result.emitToContent` called by addon script
-    // Bug 758203: We have to explicitely define `__exposedProps__` in order
-    // to allow access to these chrome object attributes from this sandbox with
-    // content priviledges
-    // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
-    let chromeAPI = {
-      timers: {
-        setTimeout: timer.setTimeout,
-        setInterval: timer.setInterval,
-        clearTimeout: timer.clearTimeout,
-        clearInterval: timer.clearInterval,
-        __exposedProps__: {
-          setTimeout: 'r',
-          setInterval: 'r',
-          clearTimeout: 'r',
-          clearInterval: 'r'
-        }
-      },
-      sandbox: {
-        evaluate: evaluate,
-        __exposedProps__: {
-          evaluate: 'r',
-        }
-      },
-      __exposedProps__: {
-        timers: 'r',
-        sandbox: 'r',
-      }
-    };
-    let onEvent = this._onContentEvent.bind(this);
-    // `ContentWorker` is defined in CONTENT_WORKER_URL file
-    let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
-    this._emitToContent = result.emitToContent;
-    this._hasListenerFor = result.hasListenerFor;
-
-    // Handle messages send by this script:
-    let self = this;
-    // console.xxx calls
-    this.on("console", function consoleListener(kind) {
-      console[kind].apply(console, Array.slice(arguments, 1));
-    });
-
-    // self.postMessage calls
-    this.on("message", function postMessage(data) {
-      // destroyed?
-      if (self._addonWorker)
-        self._addonWorker._emit('message', data);
-    });
-
-    // self.port.emit calls
-    this.on("event", function portEmit(name, args) {
-      // destroyed?
-      if (self._addonWorker)
-        self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments);
-    });
-
-    // unwrap, recreate and propagate async Errors thrown from content-script
-    this.on("error", function onError({instanceOfError, value}) {
-      if (self._addonWorker) {
-        let error = value;
-        if (instanceOfError) {
-          error = new Error(value.message, value.fileName, value.lineNumber);
-          error.stack = value.stack;
-          error.name = value.name;
-        }
-        self._addonWorker._emit('error', error);
-      }
-    });
-
-    // Inject `addon` global into target document if document is trusted,
-    // `addon` in document is equivalent to `self` in content script.
-    if (worker._injectInDocument) {
-      let win = window.wrappedJSObject ? window.wrappedJSObject : window;
-      Object.defineProperty(win, "addon", {
-          value: content.self
-        }
-      );
-    }
-
-    // Inject our `console` into target document if worker doesn't have a tab
-    // (e.g Panel, PageWorker, Widget).
-    // `worker.tab` can't be used because bug 804935.
-    if (!getTabForContentWindow(window)) {
-      let win = window.wrappedJSObject ? window.wrappedJSObject : window;
-
-      // export our chrome console to content window, using the same approach
-      // of `ConsoleAPI`:
-      // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
-      //
-      // and described here:
-      // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
-      let con = Cu.createObjectIn(win);
-
-      let genPropDesc = function genPropDesc(fun) {
-        return { enumerable: true, configurable: true, writable: true,
-          value: console[fun] };
-      }
-
-      const properties = {
-        log: genPropDesc('log'),
-        info: genPropDesc('info'),
-        warn: genPropDesc('warn'),
-        error: genPropDesc('error'),
-        debug: genPropDesc('debug'),
-        trace: genPropDesc('trace'),
-        dir: genPropDesc('dir'),
-        group: genPropDesc('group'),
-        groupCollapsed: genPropDesc('groupCollapsed'),
-        groupEnd: genPropDesc('groupEnd'),
-        time: genPropDesc('time'),
-        timeEnd: genPropDesc('timeEnd'),
-        profile: genPropDesc('profile'),
-        profileEnd: genPropDesc('profileEnd'),
-       __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
-                            value: function() {} }
-      };
-
-      Object.defineProperties(con, properties);
-      Cu.makeObjectPropsNormal(con);
-
-      win.console = con;
-    };
-
-    // The order of `contentScriptFile` and `contentScript` evaluation is
-    // intentional, so programs can load libraries like jQuery from script URLs
-    // and use them in scripts.
-    let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
-          : null,
-        contentScript = ('contentScript' in worker) ? worker.contentScript : null;
-
-    if (contentScriptFile) {
-      if (Array.isArray(contentScriptFile))
-        this._importScripts.apply(this, contentScriptFile);
-      else
-        this._importScripts(contentScriptFile);
-    }
-    if (contentScript) {
-      this._evaluate(
-        Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
-      );
-    }
-  },
-  destroy: function destroy() {
-    this.emitSync("detach");
-    this._sandbox = null;
-    this._addonWorker = null;
-  },
-
-  /**
-   * JavaScript sandbox where all the content scripts are evaluated.
-   * {Sandbox}
-   */
-  _sandbox: null,
-
-  /**
-   * Reference to the addon side of the worker.
-   * @type {Worker}
-   */
-  _addonWorker: null,
-
-  /**
-   * Evaluates code in the sandbox.
-   * @param {String} code
-   *    JavaScript source to evaluate.
-   * @param {String} [filename='javascript:' + code]
-   *    Name of the file
-   */
-  _evaluate: function(code, filename) {
-    try {
-      evaluate(this._sandbox, code, filename || 'javascript:' + code);
-    }
-    catch(e) {
-      this._addonWorker._emit('error', e);
-    }
-  },
-  /**
-   * Imports scripts to the sandbox by reading files under urls and
-   * evaluating its source. If exception occurs during evaluation
-   * `"error"` event is emitted on the worker.
-   * This is actually an analog to the `importScript` method in web
-   * workers but in our case it's not exposed even though content
-   * scripts may be able to do it synchronously since IO operation
-   * takes place in the UI process.
-   */
-  _importScripts: function _importScripts(url) {
-    let urls = Array.slice(arguments, 0);
-    for each (let contentScriptFile in urls) {
-      try {
-        let uri = URL(contentScriptFile);
-        if (uri.scheme === 'resource')
-          load(this._sandbox, String(uri));
-        else
-          throw Error("Unsupported `contentScriptFile` url: " + String(uri));
-      }
-      catch(e) {
-        this._addonWorker._emit('error', e);
-      }
-    }
-  }
-});
-
 /**
  * Message-passing facility for communication between code running
  * in the content and add-on process.
  * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
  */
-const Worker = EventEmitter.compose({
-  on: Trait.required,
-  _removeAllListeners: Trait.required,
+const Worker = Class({
+  implements: [EventTarget],
+  initialize: function WorkerConstructor (options) {
+    // Save model in weak map to not expose properties
+    let model = createModel();
+    workers.set(this, model);
+
+    options = options || {};
 
-  // List of messages fired before worker is initialized
-  get _earlyEvents() {
-    delete this._earlyEvents;
-    this._earlyEvents = [];
-    return this._earlyEvents;
+    if ('contentScriptFile' in options)
+      this.contentScriptFile = options.contentScriptFile;
+    if ('contentScriptOptions' in options)
+      this.contentScriptOptions = options.contentScriptOptions;
+    if ('contentScript' in options)
+      this.contentScript = options.contentScript;
+    if ('injectInDocument' in options)
+      this.injectInDocument = !!options.injectInDocument;
+
+    setListeners(this, options);
+
+    unload.ensure(this, "destroy");
+
+    // Ensure that worker.port is initialized for contentWorker to be able
+    // to send events during worker initialization.
+    this.port = createPort(this);
+
+    model.documentUnload = documentUnload.bind(this);
+    model.pageShow = pageShow.bind(this);
+    model.pageHide = pageHide.bind(this);
+
+    if ('window' in options)
+      attach(this, options.window);
   },
 
   /**
    * Sends a message to the worker's global scope. Method takes single
    * argument, which represents data to be sent to the worker. The data may
    * be any primitive type value or `JSON`. Call of this method asynchronously
    * emits `message` event with data value in the global scope of this
    * symbiont.
    *
    * `message` event listeners can be set either by calling
    * `self.on` with a first argument string `"message"` or by
    * implementing `onMessage` function in the global scope of this worker.
    * @param {Number|String|JSON} data
    */
-  postMessage: function (data) {
-    let args = ['message'].concat(Array.slice(arguments));
-    if (!this._inited) {
-      this._earlyEvents.push(args);
+  postMessage: function (...data) {
+    let model = modelFor(this);
+    let args = ['message'].concat(data);
+    if (!model.inited) {
+      model.earlyEvents.push(args);
       return;
     }
-    processMessage.apply(this, args);
-  },
-
-  /**
-   * EventEmitter, that behaves (calls listeners) asynchronously.
-   * A way to send customized messages to / from the worker.
-   * Events from in the worker can be observed / emitted via
-   * worker.on / worker.emit.
-   */
-  get port() {
-    // We generate dynamically this attribute as it needs to be accessible
-    // before Worker.constructor gets called. (For ex: Panel)
-
-    // create an event emitter that receive and send events from/to the worker
-    this._port = EventEmitterTrait.create({
-      emit: this._emitEventToContent.bind(this)
-    });
-
-    // expose wrapped port, that exposes only public properties:
-    // We need to destroy this getter in order to be able to set the
-    // final value. We need to update only public port attribute as we never
-    // try to access port attribute from private API.
-    delete this._public.port;
-    this._public.port = Cortex(this._port);
-    // Replicate public port to the private object
-    delete this.port;
-    this.port = this._public.port;
-
-    return this._port;
-  },
-
-  /**
-   * Same object than this.port but private API.
-   * Allow access to _emit, in order to send event to port.
-   */
-  _port: null,
-
-  /**
-   * Emit a custom event to the content script,
-   * i.e. emit this event on `self.port`
-   */
-  _emitEventToContent: function () {
-    let args = ['event'].concat(Array.slice(arguments));
-    if (!this._inited) {
-      this._earlyEvents.push(args);
-      return;
-    }
-    processMessage.apply(this, args);
+    processMessage.apply(null, [this].concat(args));
   },
 
-  // Is worker connected to the content worker sandbox ?
-  _inited: false,
-
-  // Is worker being frozen? i.e related document is frozen in bfcache.
-  // Content script should not be reachable if frozen.
-  _frozen: true,
-
-  constructor: function Worker(options) {
-    options = options || {};
-
-    if ('contentScriptFile' in options)
-      this.contentScriptFile = options.contentScriptFile;
-    if ('contentScriptOptions' in options)
-      this.contentScriptOptions = options.contentScriptOptions;
-    if ('contentScript' in options)
-      this.contentScript = options.contentScript;
-
-    this._setListeners(options);
-
-    unload.ensure(this._public, "destroy");
-
-    // Ensure that worker._port is initialized for contentWorker to be able
-    // to send events during worker initialization.
-    this.port;
-
-    this._documentUnload = this._documentUnload.bind(this);
-    this._pageShow = this._pageShow.bind(this);
-    this._pageHide = this._pageHide.bind(this);
-
-    if ("window" in options) this._attach(options.window);
-  },
-
-  _setListeners: function(options) {
-    if ('onError' in options)
-      this.on('error', options.onError);
-    if ('onMessage' in options)
-      this.on('message', options.onMessage);
-    if ('onDetach' in options)
-      this.on('detach', options.onDetach);
+  get url () {
+    let model = modelFor(this);
+    // model.window will be null after detach
+    return model.window ? model.window.document.location.href : null;
   },
 
-  _attach: function(window) {
-    this._window = window;
-    // Track document unload to destroy this worker.
-    // We can't watch for unload event on page's window object as it
-    // prevents bfcache from working:
-    // https://developer.mozilla.org/En/Working_with_BFCache
-    this._windowID = getInnerId(this._window);
-    observers.add("inner-window-destroyed", this._documentUnload);
-
-    // Listen to pagehide event in order to freeze the content script
-    // while the document is frozen in bfcache:
-    this._window.addEventListener("pageshow", this._pageShow, true);
-    this._window.addEventListener("pagehide", this._pageHide, true);
-
-    // will set this._contentWorker pointing to the private API:
-    this._contentWorker = WorkerSandbox(this);
-
-    // Mainly enable worker.port.emit to send event to the content worker
-    this._inited = true;
-    this._frozen = false;
-
-    // Process all events and messages that were fired before the
-    // worker was initialized.
-    this._earlyEvents.forEach((function (args) {
-      processMessage.apply(this, args);
-    }).bind(this));
+  get contentURL () {
+    let model = modelFor(this);
+    return model.window ? model.window.document.URL : null;
   },
 
-  _documentUnload: function _documentUnload(subject, topic, data) {
-    let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
-    if (innerWinID != this._windowID) return false;
-    this._workerCleanup();
-    return true;
-  },
-
-  _pageShow: function _pageShow() {
-    this._contentWorker.emitSync("pageshow");
-    this._emit("pageshow");
-    this._frozen = false;
-  },
-
-  _pageHide: function _pageHide() {
-    this._contentWorker.emitSync("pagehide");
-    this._emit("pagehide");
-    this._frozen = true;
-  },
-
-  get url() {
-    // this._window will be null after detach
-    return this._window ? this._window.document.location.href : null;
-  },
-
-  get tab() {
-    // this._window will be null after detach
-    if (this._window)
-      return getTabForWindow(this._window);
+  get tab () {
+    let model = modelFor(this);
+    // model.window will be null after detach
+    if (model.window)
+      return getTabForWindow(model.window);
     return null;
   },
 
-  /**
-   * Tells content worker to unload itself and
-   * removes all the references from itself.
-   */
-  destroy: function destroy() {
-    this._workerCleanup();
-    this._inited = true;
-    this._removeAllListeners();
+  // Implemented to provide some of the previous features of exposing sandbox
+  // so that Worker can be extended
+  getSandbox: function () {
+    return modelFor(this).contentWorker;
   },
 
-  /**
-   * Remove all internal references to the attached document
-   * Tells _port to unload itself and removes all the references from itself.
-   */
-  _workerCleanup: function _workerCleanup() {
-    // maybe unloaded before content side is created
-    // As Symbiont call worker.constructor on document load
-    if (this._contentWorker)
-      this._contentWorker.destroy();
-    this._contentWorker = null;
-    if (this._window) {
-      this._window.removeEventListener("pageshow", this._pageShow, true);
-      this._window.removeEventListener("pagehide", this._pageHide, true);
-    }
-    this._window = null;
-    // This method may be called multiple times,
-    // avoid dispatching `detach` event more than once
-    if (this._windowID) {
-      this._windowID = null;
-      observers.remove("inner-window-destroyed", this._documentUnload);
-      this._earlyEvents.length = 0;
-      this._emit("detach");
-    }
-    this._inited = false;
-  },
+  toString: function () { return '[object Worker]'; },
+  attach: method(attach),
+  detach: method(detach),
+  destroy: method(destroy)
+});
+exports.Worker = Worker;
+
+attach.define(Worker, function (worker, window) {
+  let model = modelFor(worker);
+  model.window = window;
+  // Track document unload to destroy this worker.
+  // We can't watch for unload event on page's window object as it
+  // prevents bfcache from working:
+  // https://developer.mozilla.org/En/Working_with_BFCache
+  model.windowID = getInnerId(model.window);
+  events.on("inner-window-destroyed", model.documentUnload);
 
-  /**
-   * Receive an event from the content script that need to be sent to
-   * worker.port. Provide a way for composed object to catch all events.
-   */
-  _onContentScriptEvent: function _onContentScriptEvent() {
-    this._port._emit.apply(this._port, arguments);
-  },
+  // Listen to pagehide event in order to freeze the content script
+  // while the document is frozen in bfcache:
+  model.window.addEventListener("pageshow", model.pageShow, true);
+  model.window.addEventListener("pagehide", model.pageHide, true);
+
+  // will set model.contentWorker pointing to the private API:
+  model.contentWorker = WorkerSandbox(worker, model.window);
 
-  /**
-   * Reference to the content side of the worker.
-   * @type {WorkerGlobalScope}
-   */
-  _contentWorker: null,
+  // Mainly enable worker.port.emit to send event to the content worker
+  model.inited = true;
+  model.frozen = false;
 
-  /**
-   * Reference to the window that is accessible from
-   * the content scripts.
-   * @type {Object}
-   */
-  _window: null,
+  // Fire off `attach` event
+  emit(worker, 'attach', window);
 
-  /**
-   * Flag to enable `addon` object injection in document. (bug 612726)
-   * @type {Boolean}
-   */
-  _injectInDocument: false
+  // Process all events and messages that were fired before the
+  // worker was initialized.
+  model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args)));
 });
 
 /**
- * Fired from postMessage and _emitEventToContent, or from the _earlyMessage
+ * Remove all internal references to the attached document
+ * Tells _port to unload itself and removes all the references from itself.
+ */
+detach.define(Worker, function (worker) {
+  let model = modelFor(worker);
+  // maybe unloaded before content side is created
+  if (model.contentWorker)
+    model.contentWorker.destroy();
+  model.contentWorker = null;
+  if (model.window) {
+    model.window.removeEventListener("pageshow", model.pageShow, true);
+    model.window.removeEventListener("pagehide", model.pageHide, true);
+  }
+  model.window = null;
+  // This method may be called multiple times,
+  // avoid dispatching `detach` event more than once
+  if (model.windowID) {
+    model.windowID = null;
+    events.off("inner-window-destroyed", model.documentUnload);
+    model.earlyEvents.length = 0;
+    emit(worker, 'detach');
+  }
+  model.inited = false;
+});
+
+/**
+ * Tells content worker to unload itself and
+ * removes all the references from itself.
+ */
+destroy.define(Worker, function (worker) {
+  detach(worker);
+  modelFor(worker).inited = true;
+  // Specifying no type or listener removes all listeners
+  // from target
+  off(worker);
+});
+
+/**
+ * Events fired by workers
+ */
+function documentUnload ({ subject, data }) {
+  let model = modelFor(this);
+  let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+  if (innerWinID != model.windowID) return false;
+  detach(this);
+  return true;
+}
+
+function pageShow () {
+  let model = modelFor(this);
+  model.contentWorker.emitSync('pageshow');
+  emit(this, 'pageshow');
+  model.frozen = false;
+}
+
+function pageHide () {
+  let model = modelFor(this);
+  model.contentWorker.emitSync('pagehide');
+  emit(this, 'pagehide');
+  model.frozen = true;
+}
+
+/**
+ * Fired from postMessage and emitEventToContent, or from the earlyMessage
  * queue when fired before the content is loaded. Sends arguments to
  * contentWorker if able
  */
 
-function processMessage () {
-  if (!this._contentWorker)
+function processMessage (worker, ...args) {
+  let model = modelFor(worker) || {};
+  if (!model.contentWorker)
     throw new Error(ERR_DESTROYED);
-  if (this._frozen)
+  if (model.frozen)
     throw new Error(ERR_FROZEN);
 
-  this._contentWorker.emit.apply(null, Array.slice(arguments));
+  model.contentWorker.emit.apply(null, args);
 }
 
-exports.Worker = Worker;
+function createModel () {
+  return {
+    // List of messages fired before worker is initialized
+    earlyEvents: [],
+    // Is worker connected to the content worker sandbox ?
+    inited: false,
+    // Is worker being frozen? i.e related document is frozen in bfcache.
+    // Content script should not be reachable if frozen.
+    frozen: true,
+    /**
+     * Reference to the content side of the worker.
+     * @type {WorkerGlobalScope}
+     */
+    contentWorker: null,
+    /**
+     * Reference to the window that is accessible from
+     * the content scripts.
+     * @type {Object}
+     */
+    window: null
+  };
+}
+
+function createPort (worker) {
+  let port = EventTarget();
+  port.emit = emitEventToContent.bind(null, worker);
+  return port;
+}
+
+/**
+ * Emit a custom event to the content script,
+ * i.e. emit this event on `self.port`
+ */
+function emitEventToContent (worker, ...eventArgs) {
+  let model = modelFor(worker);
+  let args = ['event'].concat(eventArgs);
+  if (!model.inited) {
+    model.earlyEvents.push(args);
+    return;
+  }
+  processMessage.apply(null, [worker].concat(args));
+}
+
--- a/addon-sdk/source/lib/sdk/context-menu.js
+++ b/addon-sdk/source/lib/sdk/context-menu.js
@@ -358,36 +358,38 @@ let menuRules = mix(labelledItemRules, {
         return item instanceof BaseItem;
       });
     },
     msg: "items must be an array, and each element in the array must be an " +
          "Item, Menu, or Separator."
   }
 });
 
-let ContextWorker = Worker.compose({
+let ContextWorker = Class({
+  implements: [ Worker ],
+
   //Returns true if any context listeners are defined in the worker's port.
   anyContextListeners: function anyContextListeners() {
-    return this._contentWorker.hasListenerFor("context");
+    return this.getSandbox().hasListenerFor("context");
   },
 
   // Calls the context workers context listeners and returns the first result
   // that is either a string or a value that evaluates to true. If all of the
   // listeners returned false then returns false. If there are no listeners
   // then returns null.
   getMatchedContext: function getCurrentContexts(popupNode) {
-    let results = this._contentWorker.emitSync("context", popupNode);
+    let results = this.getSandbox().emitSync("context", popupNode);
     return results.reduce(function(val, result) val || result, null);
   },
 
   // Emits a click event in the worker's port. popupNode is the node that was
   // context-clicked, and clickedItemData is the data of the item that was
   // clicked.
   fireClick: function fireClick(popupNode, clickedItemData) {
-    this._contentWorker.emitSync("click", popupNode, clickedItemData);
+    this.getSandbox().emitSync("click", popupNode, clickedItemData);
   }
 });
 
 // Returns true if any contexts match. If there are no contexts then a
 // PageContext is tested instead
 function hasMatchingContext(contexts, popupNode) {
   for (let context in contexts) {
     if (!context.isCurrent(popupNode))
--- a/addon-sdk/source/lib/sdk/deprecated/api-utils.js
+++ b/addon-sdk/source/lib/sdk/deprecated/api-utils.js
@@ -23,39 +23,16 @@ const VALID_TYPES = [
   "object",
   "string",
   "undefined",
 ];
 
 const { isArray } = Array;
 
 /**
- * Returns a function C that creates instances of privateCtor.  C may be called
- * with or without the new keyword.  The prototype of each instance returned
- * from C is C.prototype, and C.prototype is an object whose prototype is
- * privateCtor.prototype.  Instances returned from C will therefore be instances
- * of both C and privateCtor.  Additionally, the constructor of each instance
- * returned from C is C.
- *
- * @param  privateCtor
- *         A constructor.
- * @return A function that makes new instances of privateCtor.
- */
-exports.publicConstructor = function publicConstructor(privateCtor) {
-  function PublicCtor() {
-    let obj = { constructor: PublicCtor, __proto__: PublicCtor.prototype };
-    memory.track(obj, privateCtor.name);
-    privateCtor.apply(obj, arguments);
-    return obj;
-  }
-  PublicCtor.prototype = { __proto__: privateCtor.prototype };
-  return PublicCtor;
-};
-
-/**
  * Returns a validated options dictionary given some requirements.  If any of
  * the requirements are not met, an exception is thrown.
  *
  * @param  options
  *         An object, the options dictionary to validate.  It's not modified.
  *         If it's null or otherwise falsey, an empty object is assumed.
  * @param  requirements
  *         An object whose keys are the expected keys in options.  Any key in
deleted file mode 100644
--- a/addon-sdk/source/lib/sdk/deprecated/app-strings.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/* 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";
-
-module.metadata = {
-  "stability": "deprecated"
-};
-
-const {Cc,Ci} = require("chrome");
-const apiUtils = require("./api-utils");
-
-/**
- * A bundle of strings.
- *
- * @param url {String}
- *        the URL of the string bundle
- */
-exports.StringBundle = apiUtils.publicConstructor(function StringBundle(url) {
-
-  let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
-                     getService(Ci.nsIStringBundleService).
-                     createBundle(url);
-
-  this.__defineGetter__("url", function () url);
-
-  /**
-   * Get a string from the bundle.
-   *
-   * @param name {String}
-   *        the name of the string to get
-   * @param args {array} [optional]
-   *        an array of arguments that replace occurrences of %S in the string
-   *
-   * @returns {String} the value of the string
-   */
-  this.get = function strings_get(name, args) {
-    try {
-      if (args)
-        return stringBundle.formatStringFromName(name, args, args.length);
-      else
-        return stringBundle.GetStringFromName(name);
-    }
-    catch(ex) {
-      // f.e. "Component returned failure code: 0x80004005 (NS_ERROR_FAILURE)
-      // [nsIStringBundle.GetStringFromName]"
-      throw new Error("String '" + name + "' could not be retrieved from the " +
-                      "bundle due to an unknown error (it doesn't exist?).");
-    }
-  },
-
-  /**
-   * Iterate the strings in the bundle.
-   *
-   */
-  apiUtils.addIterator(
-    this,
-    function keysValsGen() {
-      let enumerator = stringBundle.getSimpleEnumeration();
-      while (enumerator.hasMoreElements()) {
-        let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
-        yield [elem.key, elem.value];
-      }
-    }
-  );
-});
--- a/addon-sdk/source/lib/sdk/deprecated/memory.js
+++ b/addon-sdk/source/lib/sdk/deprecated/memory.js
@@ -1,53 +1,57 @@
 /* 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";
 
 module.metadata = {
   "stability": "deprecated"
 };
 
-const {Cc,Ci,Cu,components} = require("chrome");
-var trackedObjects = {};
+const { Cc, Ci, Cu, components } = require("chrome");
+const { when: unload } = require("../system/unload")
 
-var Compacter = {
-  INTERVAL: 5000,
-  notify: function(timer) {
+var trackedObjects = {};
+const Compacter = {
+  notify: function() {
     var newTrackedObjects = {};
+
     for (let name in trackedObjects) {
-      var oldBin = trackedObjects[name];
-      var newBin = [];
-      var strongRefs = [];
-      for (var i = 0; i < oldBin.length; i++) {
-        var strongRef = oldBin[i].weakref.get();
+      let oldBin = trackedObjects[name];
+      let newBin = [];
+      let strongRefs = [];
+
+      for (let i = 0, l = oldBin.length; i < l; i++) {
+        let strongRef = oldBin[i].weakref.get();
+
         if (strongRef && strongRefs.indexOf(strongRef) == -1) {
           strongRefs.push(strongRef);
           newBin.push(oldBin[i]);
         }
       }
+
       if (newBin.length)
         newTrackedObjects[name] = newBin;
     }
+
     trackedObjects = newTrackedObjects;
   }
 };
 
 var timer = Cc["@mozilla.org/timer;1"]
             .createInstance(Ci.nsITimer);
-
 timer.initWithCallback(Compacter,
-                       Compacter.INTERVAL,
+                       5000,
                        Ci.nsITimer.TYPE_REPEATING_SLACK);
 
-var track = exports.track = function track(object, bin, stackFrameNumber) {
+function track(object, bin, stackFrameNumber) {
   var frame = components.stack.caller;
   var weakref = Cu.getWeakReference(object);
+
   if (!bin && 'constructor' in object)
     bin = object.constructor.name;
   if (bin == "Object")
     bin = frame.name;
   if (!bin)
     bin = "generic";
   if (!(bin in trackedObjects))
     trackedObjects[bin] = [];
@@ -56,63 +60,70 @@ var track = exports.track = function tra
     for (var i = 0; i < stackFrameNumber; i++)
       frame = frame.caller;
 
   trackedObjects[bin].push({weakref: weakref,
                             created: new Date(),
                             filename: frame.filename,
                             lineNo: frame.lineNumber,
                             bin: bin});
-};
+}
+exports.track = track;
 
 var getBins = exports.getBins = function getBins() {
   var names = [];
   for (let name in trackedObjects)
     names.push(name);
   return names;
 };
 
-var getObjects = exports.getObjects = function getObjects(bin) {
-  function getLiveObjectsInBin(bin, array) {
-    for (var i = 0; i < bin.length; i++) {
-      var object = bin[i].weakref.get();
-      if (object)
-        array.push(bin[i]);
+function getObjects(bin) {
+  var results = [];
+
+  function getLiveObjectsInBin(bin) {
+    for (let i = 0, l = bin.length; i < l; i++) {
+      let object = bin[i].weakref.get();
+
+      if (object) {
+        results.push(bin[i]);
+      }
     }
   }
 
-  var results = [];
   if (bin) {
     if (bin in trackedObjects)
-      getLiveObjectsInBin(trackedObjects[bin], results);
-  } else
+      getLiveObjectsInBin(trackedObjects[bin]);
+  }
+  else {
     for (let name in trackedObjects)
-      getLiveObjectsInBin(trackedObjects[name], results);
+      getLiveObjectsInBin(trackedObjects[name]);
+  }
+
   return results;
-};
+}
+exports.getObjects = getObjects;
 
-var gc = exports.gc = function gc() {
+function gc() {
   // Components.utils.forceGC() doesn't currently perform
   // cycle collection, which means that e.g. DOM elements
   // won't be collected by it. Fortunately, there are
   // other ways...
-
-  var window = Cc["@mozilla.org/appshell/appShellService;1"]
+  var test_utils = Cc["@mozilla.org/appshell/appShellService;1"]
                .getService(Ci.nsIAppShellService)
-               .hiddenDOMWindow;
-  var test_utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                   .getInterface(Ci.nsIDOMWindowUtils);
+               .hiddenDOMWindow
+               .QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindowUtils);
   test_utils.garbageCollect();
+  // Clean metadata for dead objects
   Compacter.notify();
-
   // Not sure why, but sometimes it appears that we don't get
   // them all with just one CC, so let's do it again.
   test_utils.garbageCollect();
 };
+exports.gc = gc;
 
-require("../system/unload").when(
-  function() {
-    trackedObjects = {};
-    if (timer) {
-      timer.cancel();
-      timer = null;
-    }
-  });
+unload(_ => {
+  trackedObjects = {};
+  if (timer) {
+    timer.cancel();
+    timer = null;
+  }
+});
deleted file mode 100644
--- a/addon-sdk/source/lib/sdk/deprecated/observer-service.js
+++ /dev/null
@@ -1,134 +0,0 @@
-/* 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";
-
-module.metadata = {
-  "stability": "deprecated"
-};
-
-const { Cc, Ci } = require("chrome");
-const { when: unload } = require("../system/unload");
-const { ns } = require("../core/namespace");
-const { on, off, emit, once } = require("../system/events");
-const { id } = require("../self");
-
-const subscribers = ns();
-const cache = [];
-
-/**
- * Topics specifically available to Jetpack-generated extensions.
- *
- * Using these predefined consts instead of the platform strings is good:
- *   - allows us to scope topics specifically for Jetpacks
- *   - addons aren't dependent on strings nor behavior of core platform topics
- *   - the core platform topics are not clearly named
- *
- */
-exports.topics = {
-  /**
-   * A topic indicating that the application is in a state usable
-   * by add-ons.
-   */
-  APPLICATION_READY: id + "_APPLICATION_READY"
-};
-
-function Listener(callback, target) {
-  return function listener({ subject, data }) {
-    callback.call(target || callback, subject, data);
-  }
-}
-
-/**
- * Register the given callback as an observer of the given topic.
- *
- * @param   topic       {String}
- *          the topic to observe
- *
- * @param   callback    {Object}
- *          the callback; an Object that implements nsIObserver or a Function
- *          that gets called when the notification occurs
- *
- * @param   target  {Object}  [optional]
- *          the object to use as |this| when calling a Function callback
- *
- * @returns the observer
- */
-function add(topic, callback, target) {
-  let listeners = subscribers(callback);
-  if (!(topic in listeners)) {
-    let listener = Listener(callback, target);
-    listeners[topic] = listener;
-
-    // Cache callback unless it's already cached.
-    if (!~cache.indexOf(callback))
-      cache.push(callback);
-
-    on(topic, listener);
-  }
-};
-exports.add = add;
-
-/**
- * Unregister the given callback as an observer of the given topic.
- *
- * @param   topic       {String}
- *          the topic being observed
- *
- * @param   callback    {Object}
- *          the callback doing the observing
- *
- * @param   target  {Object}  [optional]
- *          the object being used as |this| when calling a Function callback
- */
-function remove(topic, callback, target) {
-  let listeners = subscribers(callback);
-  if (topic in listeners) {
-    let listener = listeners[topic];
-    delete listeners[topic];
-
-    // If no more observers are registered and callback is still in cache
-    // then remove it.
-    let index = cache.indexOf(callback);
-    if (~index && !Object.keys(listeners).length)
-      cache.splice(index, 1)
-
-    off(topic, listener);
-  }
-};
-exports.remove = remove;
-
-/**
- * Notify observers about something.
- *
- * @param topic   {String}
- *        the topic to notify observers about
- *
- * @param subject {Object}  [optional]
- *        some information about the topic; can be any JS object or primitive
- *
- * @param data    {String}  [optional] [deprecated]
- *        some more information about the topic; deprecated as the subject
- *        is sufficient to pass all needed information to the JS observers
- *        that this module targets; if you have multiple values to pass to
- *        the observer, wrap them in an object and pass them via the subject
- *        parameter (i.e.: { foo: 1, bar: "some string", baz: myObject })
- */
-function notify(topic, subject, data) {
-  emit(topic, {
-    subject: subject === undefined ? null : subject,
-    data: data === undefined ? null : data
-  });
-}
-exports.notify = notify;
-
-unload(function() {
-  // Make a copy of cache first, since cache will be changing as we
-  // iterate through it.
-  cache.slice().forEach(function(callback) {
-    Object.keys(subscribers(callback)).forEach(function(topic) {
-      remove(topic, callback);
-    });
-  });
-})
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/deprecated/symbiont.js
@@ -0,0 +1,229 @@
+/* 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";
+
+module.metadata = {
+  "stability": "deprecated"
+};
+
+const { Worker } = require('./traits-worker');
+const { Loader } = require('../content/loader');
+const hiddenFrames = require('../frame/hidden-frame');
+const { on, off } = require('../system/events');
+const unload = require('../system/unload');
+const { getDocShell } = require("../frame/utils");
+const { ignoreWindow } = require('../private-browsing/utils');
+
+// Everything coming from add-on's xpi considered an asset.
+const assetsURI = require('../self').data.url().replace(/data\/$/, "");
+
+/**
+ * This trait is layered on top of `Worker` and in contrast to symbiont
+ * Worker constructor requires `content` option that represents content
+ * that will be loaded in the provided frame, if frame is not provided
+ * Worker will create hidden one.
+ */
+const Symbiont = Worker.resolve({
+    constructor: '_initWorker',
+    destroy: '_workerDestroy'
+  }).compose(Loader, {
+
+  /**
+   * The constructor requires all the options that are required by
+   * `require('content').Worker` with the difference that the `frame` option
+   * is optional. If `frame` is not provided, `contentURL` is expected.
+   * @param {Object} options
+   * @param {String} options.contentURL
+   *    URL of a content to load into `this._frame` and create worker for.
+   * @param {Element} [options.frame]
+   *    iframe element that is used to load `options.contentURL` into.
+   *    if frame is not provided hidden iframe will be created.
+   */
+  constructor: function Symbiont(options) {
+    options = options || {};
+
+    if ('contentURL' in options)
+        this.contentURL = options.contentURL;
+    if ('contentScriptWhen' in options)
+      this.contentScriptWhen = options.contentScriptWhen;
+    if ('contentScriptOptions' in options)
+      this.contentScriptOptions = options.contentScriptOptions;
+    if ('contentScriptFile' in options)
+      this.contentScriptFile = options.contentScriptFile;
+    if ('contentScript' in options)
+      this.contentScript = options.contentScript;
+    if ('allow' in options)
+      this.allow = options.allow;
+    if ('onError' in options)
+        this.on('error', options.onError);
+    if ('onMessage' in options)
+        this.on('message', options.onMessage);
+    if ('frame' in options) {
+      this._initFrame(options.frame);
+    }
+    else {
+      let self = this;
+      this._hiddenFrame = hiddenFrames.HiddenFrame({
+        onReady: function onFrame() {
+          self._initFrame(this.element);
+        },
+        onUnload: function onUnload() {
+          // Bug 751211: Remove reference to _frame when hidden frame is
+          // automatically removed on unload, otherwise we are going to face
+          // "dead object" exception
+          self.destroy();
+        }
+      });
+      hiddenFrames.add(this._hiddenFrame);
+    }
+
+    unload.ensure(this._public, "destroy");
+  },
+
+  destroy: function destroy() {
+    this._workerDestroy();
+    this._unregisterListener();
+    this._frame = null;
+    if (this._hiddenFrame) {
+      hiddenFrames.remove(this._hiddenFrame);
+      this._hiddenFrame = null;
+    }
+  },
+
+  /**
+   * XUL iframe or browser elements with attribute `type` being `content`.
+   * Used to create `ContentSymbiont` from.
+   * @type {nsIFrame|nsIBrowser}
+   */
+  _frame: null,
+
+  /**
+   * Listener to the `'frameReady"` event (emitted when `iframe` is ready).
+   * Removes listener, sets right permissions to the frame and loads content.
+   */
+  _initFrame: function _initFrame(frame) {
+    if (this._loadListener)
+      this._unregisterListener();
+
+    this._frame = frame;
+
+    if (getDocShell(frame)) {
+      this._reallyInitFrame(frame);
+    }
+    else {
+      if (this._waitForFrame) {
+        off('content-document-global-created', this._waitForFrame);
+      }
+      this._waitForFrame = this.__waitForFrame.bind(this, frame);
+      on('content-document-global-created', this._waitForFrame);
+    }
+  },
+
+  __waitForFrame: function _waitForFrame(frame, { subject: win }) {
+    if (frame.contentWindow == win) {
+      off('content-document-global-created', this._waitForFrame);
+      delete this._waitForFrame;
+      this._reallyInitFrame(frame);
+    }
+  },
+
+  _reallyInitFrame: function _reallyInitFrame(frame) {
+    getDocShell(frame).allowJavascript = this.allow.script;
+    frame.setAttribute("src", this._contentURL);
+
+    // Inject `addon` object in document if we load a document from
+    // one of our addon folder and if no content script are defined. bug 612726
+    let isDataResource =
+      typeof this._contentURL == "string" &&
+      this._contentURL.indexOf(assetsURI) == 0;
+    let hasContentScript =
+      (Array.isArray(this.contentScript) ? this.contentScript.length > 0
+                                             : !!this.contentScript) ||
+      (Array.isArray(this.contentScriptFile) ? this.contentScriptFile.length > 0
+                                             : !!this.contentScriptFile);
+    // If we have to inject `addon` we have to do it before document
+    // script execution, so during `start`:
+    this._injectInDocument = isDataResource && !hasContentScript;
+    if (this._injectInDocument)
+      this.contentScriptWhen = "start";
+
+    if ((frame.contentDocument.readyState == "complete" ||
+        (frame.contentDocument.readyState == "interactive" &&
+         this.contentScriptWhen != 'end' )) &&
+        frame.contentDocument.location == this._contentURL) {
+      // In some cases src doesn't change and document is already ready
+      // (for ex: when the user moves a widget while customizing toolbars.)
+      this._onInit();
+      return;
+    }
+
+    let self = this;
+
+    if ('start' == this.contentScriptWhen) {
+      this._loadEvent = 'start';
+      on('document-element-inserted',
+        this._loadListener = function onStart({ subject: doc }) {
+          let window = doc.defaultView;
+
+          if (ignoreWindow(window)) {
+            return;
+          }
+
+          if (window && window == frame.contentWindow) {
+            self._unregisterListener();
+            self._onInit();
+          }
+
+        });
+      return;
+    }
+
+    let eventName = 'end' == this.contentScriptWhen ? 'load' : 'DOMContentLoaded';
+    let self = this;
+    this._loadEvent = eventName;
+    frame.addEventListener(eventName,
+      this._loadListener = function _onReady(event) {
+
+        if (event.target != frame.contentDocument)
+          return;
+        self._unregisterListener();
+
+        self._onInit();
+
+      }, true);
+
+  },
+
+  /**
+   * Unregister listener that watchs for document being ready to be injected.
+   * This listener is registered in `Symbiont._initFrame`.
+   */
+  _unregisterListener: function _unregisterListener() {
+    if (this._waitForFrame) {
+      off('content-document-global-created', this._waitForFrame);
+      delete this._waitForFrame;
+    }
+
+    if (!this._loadListener)
+      return;
+    if (this._loadEvent == "start") {
+      off('document-element-inserted', this._loadListener);
+    }
+    else {
+      this._frame.removeEventListener(this._loadEvent, this._loadListener,
+                                      true);
+    }
+    this._loadListener = null;
+  },
+
+  /**
+   * Called by Symbiont itself when the frame is ready to load
+   * content scripts according to contentScriptWhen. Overloaded by Panel.
+   */
+  _onInit: function () {
+    this._initWorker({ window: this._frame.contentWindow });
+  }
+
+});
+exports.Symbiont = Symbiont;
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/deprecated/traits-worker.js
@@ -0,0 +1,660 @@
+/* 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/. */
+
+/**
+ *
+ * `deprecated/traits-worker` was previously `content/worker` and kept
+ * only due to `deprecated/symbiont` using it, which is necessary for
+ * `widget`, until that reaches deprecation EOL.
+ *
+ */
+
+"use strict";
+
+module.metadata = {
+  "stability": "deprecated"
+};
+
+const { Trait } = require('./traits');
+const { EventEmitter, EventEmitterTrait } = require('./events');
+const { Ci, Cu, Cc } = require('chrome');
+const timer = require('../timers');
+const { URL } = require('../url');
+const unload = require('../system/unload');
+const observers = require('../system/events');
+const { Cortex } = require('./cortex');
+const { sandbox, evaluate, load } = require("../loader/sandbox");
+const { merge } = require('../util/object');
+const xulApp = require("../system/xul-app");
+const { getInnerId } = require("../window/utils")
+const USE_JS_PROXIES = !xulApp.versionInRange(xulApp.platformVersion,
+                                              "17.0a2", "*");
+const { getTabForWindow } = require('../tabs/helpers');
+const { getTabForContentWindow } = require('../tabs/utils');
+
+/* Trick the linker in order to ensure shipping these files in the XPI.
+  require('../content/content-worker.js');
+  Then, retrieve URL of these files in the XPI:
+*/
+let prefix = module.uri.split('deprecated/traits-worker.js')[0];
+const CONTENT_WORKER_URL = prefix + 'content/content-worker.js';
+
+// Fetch additional list of domains to authorize access to for each content
+// script. It is stored in manifest `metadata` field which contains
+// package.json data. This list is originaly defined by authors in
+// `permissions` attribute of their package.json addon file.
+const permissions = require('@loader/options').metadata['permissions'] || {};
+const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || [];
+
+const JS_VERSION = '1.8';
+
+const ERR_DESTROYED =
+  "Couldn't find the worker to receive this message. " +
+  "The script may not be initialized yet, or may already have been unloaded.";
+
+const ERR_FROZEN = "The page is currently hidden and can no longer be used " +
+                   "until it is visible again.";
+
+
+const WorkerSandbox = EventEmitter.compose({
+
+  /**
+   * Emit a message to the worker content sandbox
+   */
+  emit: function emit() {
+    // First ensure having a regular array
+    // (otherwise, `arguments` would be mapped to an object by `stringify`)
+    let array = Array.slice(arguments);
+    // JSON.stringify is buggy with cross-sandbox values,
+    // it may return "{}" on functions. Use a replacer to match them correctly.
+    function replacer(k, v) {
+      return typeof v === "function" ? undefined : v;
+    }
+    // Ensure having an asynchronous behavior
+    let self = this;
+    timer.setTimeout(function () {
+      self._emitToContent(JSON.stringify(array, replacer));
+    }, 0);
+  },
+
+  /**
+   * Synchronous version of `emit`.
+   * /!\ Should only be used when it is strictly mandatory /!\
+   *     Doesn't ensure passing only JSON values.
+   *     Mainly used by context-menu in order to avoid breaking it.
+   */
+  emitSync: function emitSync() {
+    let args = Array.slice(arguments);
+    return this._emitToContent(args);
+  },
+
+  /**
+   * Tells if content script has at least one listener registered for one event,
+   * through `self.on('xxx', ...)`.
+   * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API.
+   */
+  hasListenerFor: function hasListenerFor(name) {
+    return this._hasListenerFor(name);
+  },
+
+  /**
+   * Method called by the worker sandbox when it needs to send a message
+   */
+  _onContentEvent: function onContentEvent(args) {
+    // As `emit`, we ensure having an asynchronous behavior
+    let self = this;
+    timer.setTimeout(function () {
+      // We emit event to chrome/addon listeners
+      self._emit.apply(self, JSON.parse(args));
+    }, 0);
+  },
+
+  /**
+   * Configures sandbox and loads content scripts into it.
+   * @param {Worker} worker
+   *    content worker
+   */
+  constructor: function WorkerSandbox(worker) {
+    this._addonWorker = worker;
+
+    // Ensure that `emit` has always the right `this`
+    this.emit = this.emit.bind(this);
+    this.emitSync = this.emitSync.bind(this);
+
+    // We receive a wrapped window, that may be an xraywrapper if it's content
+    let window = worker._window;
+    let proto = window;
+
+    // Eventually use expanded principal sandbox feature, if some are given.
+    //
+    // But prevent it when the Worker isn't used for a content script but for
+    // injecting `addon` object into a Panel, Widget, ... scope.
+    // That's because:
+    // 1/ It is useless to use multiple domains as the worker is only used
+    // to communicate with the addon,
+    // 2/ By using it it would prevent the document to have access to any JS
+    // value of the worker. As JS values coming from multiple domain principals
+    // can't be accessed by "mono-principals" (principal with only one domain).
+    // Even if this principal is for a domain that is specified in the multiple
+    // domain principal.
+    let principals  = window;
+    let wantGlobalProperties = []
+    if (EXPANDED_PRINCIPALS.length > 0 && !worker._injectInDocument) {
+      principals = EXPANDED_PRINCIPALS.concat(window);
+      // We have to replace XHR constructor of the content document
+      // with a custom cross origin one, automagically added by platform code:
+      delete proto.XMLHttpRequest;
+      wantGlobalProperties.push("XMLHttpRequest");
+    }
+
+    // Instantiate trusted code in another Sandbox in order to prevent content
+    // script from messing with standard classes used by proxy and API code.
+    let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window });
+    apiSandbox.console = console;
+
+    // Create the sandbox and bind it to window in order for content scripts to
+    // have access to all standard globals (window, document, ...)
+    let content = this._sandbox = sandbox(principals, {
+      sandboxPrototype: proto,
+      wantXrays: true,
+      wantGlobalProperties: wantGlobalProperties,
+      sameZoneAs: window,
+      metadata: { SDKContentScript: true }
+    });
+    // We have to ensure that window.top and window.parent are the exact same
+    // object than window object, i.e. the sandbox global object. But not
+    // always, in case of iframes, top and parent are another window object.
+    let top = window.top === window ? content : content.top;
+    let parent = window.parent === window ? content : content.parent;
+    merge(content, {
+      // We need "this === window === top" to be true in toplevel scope:
+      get window() content,
+      get top() top,
+      get parent() parent,
+      // Use the Greasemonkey naming convention to provide access to the
+      // unwrapped window object so the content script can access document
+      // JavaScript values.
+      // NOTE: this functionality is experimental and may change or go away
+      // at any time!
+      get unsafeWindow() window.wrappedJSObject
+    });
+
+    // Load trusted code that will inject content script API.
+    // We need to expose JS objects defined in same principal in order to
+    // avoid having any kind of wrapper.
+    load(apiSandbox, CONTENT_WORKER_URL);
+
+    // prepare a clean `self.options`
+    let options = 'contentScriptOptions' in worker ?
+      JSON.stringify( worker.contentScriptOptions ) :
+      undefined;
+
+    // Then call `inject` method and communicate with this script
+    // by trading two methods that allow to send events to the other side:
+    //   - `onEvent` called by content script
+    //   - `result.emitToContent` called by addon script
+    // Bug 758203: We have to explicitely define `__exposedProps__` in order
+    // to allow access to these chrome object attributes from this sandbox with
+    // content priviledges
+    // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers
+    let chromeAPI = {
+      timers: {
+        setTimeout: timer.setTimeout,
+        setInterval: timer.setInterval,
+        clearTimeout: timer.clearTimeout,
+        clearInterval: timer.clearInterval,
+        __exposedProps__: {
+          setTimeout: 'r',
+          setInterval: 'r',
+          clearTimeout: 'r',
+          clearInterval: 'r'
+        }
+      },
+      sandbox: {
+        evaluate: evaluate,
+        __exposedProps__: {
+          evaluate: 'r',
+        }
+      },
+      __exposedProps__: {
+        timers: 'r',
+        sandbox: 'r',
+      }
+    };
+    let onEvent = this._onContentEvent.bind(this);
+    // `ContentWorker` is defined in CONTENT_WORKER_URL file
+    let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options);
+    this._emitToContent = result.emitToContent;
+    this._hasListenerFor = result.hasListenerFor;
+
+    // Handle messages send by this script:
+    let self = this;
+    // console.xxx calls
+    this.on("console", function consoleListener(kind) {
+      console[kind].apply(console, Array.slice(arguments, 1));
+    });
+
+    // self.postMessage calls
+    this.on("message", function postMessage(data) {
+      // destroyed?
+      if (self._addonWorker)
+        self._addonWorker._emit('message', data);
+    });
+
+    // self.port.emit calls
+    this.on("event", function portEmit(name, args) {
+      // destroyed?
+      if (self._addonWorker)
+        self._addonWorker._onContentScriptEvent.apply(self._addonWorker, arguments);
+    });
+
+    // unwrap, recreate and propagate async Errors thrown from content-script
+    this.on("error", function onError({instanceOfError, value}) {
+      if (self._addonWorker) {
+        let error = value;
+        if (instanceOfError) {
+          error = new Error(value.message, value.fileName, value.lineNumber);
+          error.stack = value.stack;
+          error.name = value.name;
+        }
+        self._addonWorker._emit('error', error);
+      }
+    });
+
+    // Inject `addon` global into target document if document is trusted,
+    // `addon` in document is equivalent to `self` in content script.
+    if (worker._injectInDocument) {
+      let win = window.wrappedJSObject ? window.wrappedJSObject : window;
+      Object.defineProperty(win, "addon", {
+          value: content.self
+        }
+      );
+    }
+
+    // Inject our `console` into target document if worker doesn't have a tab
+    // (e.g Panel, PageWorker, Widget).
+    // `worker.tab` can't be used because bug 804935.
+    if (!getTabForContentWindow(window)) {
+      let win = window.wrappedJSObject ? window.wrappedJSObject : window;
+
+      // export our chrome console to content window, using the same approach
+      // of `ConsoleAPI`:
+      // http://mxr.mozilla.org/mozilla-central/source/dom/base/ConsoleAPI.js#150
+      //
+      // and described here:
+      // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn
+      let con = Cu.createObjectIn(win);
+
+      let genPropDesc = function genPropDesc(fun) {
+        return { enumerable: true, configurable: true, writable: true,
+          value: console[fun] };
+      }
+
+      const properties = {
+        log: genPropDesc('log'),
+        info: genPropDesc('info'),
+        warn: genPropDesc('warn'),
+        error: genPropDesc('error'),
+        debug: genPropDesc('debug'),
+        trace: genPropDesc('trace'),
+        dir: genPropDesc('dir'),
+        group: genPropDesc('group'),
+        groupCollapsed: genPropDesc('groupCollapsed'),
+        groupEnd: genPropDesc('groupEnd'),
+        time: genPropDesc('time'),
+        timeEnd: genPropDesc('timeEnd'),
+        profile: genPropDesc('profile'),
+        profileEnd: genPropDesc('profileEnd'),
+       __noSuchMethod__: { enumerable: true, configurable: true, writable: true,
+                            value: function() {} }
+      };
+
+      Object.defineProperties(con, properties);
+      Cu.makeObjectPropsNormal(con);
+
+      win.console = con;
+    };
+
+    // The order of `contentScriptFile` and `contentScript` evaluation is
+    // intentional, so programs can load libraries like jQuery from script URLs
+    // and use them in scripts.
+    let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile
+          : null,
+        contentScript = ('contentScript' in worker) ? worker.contentScript : null;
+
+    if (contentScriptFile) {
+      if (Array.isArray(contentScriptFile))
+        this._importScripts.apply(this, contentScriptFile);
+      else
+        this._importScripts(contentScriptFile);
+    }
+    if (contentScript) {
+      this._evaluate(
+        Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript
+      );
+    }
+  },
+  destroy: function destroy() {
+    this.emitSync("detach");
+    this._sandbox = null;
+    this._addonWorker = null;
+  },
+
+  /**
+   * JavaScript sandbox where all the content scripts are evaluated.
+   * {Sandbox}
+   */
+  _sandbox: null,
+
+  /**
+   * Reference to the addon side of the worker.
+   * @type {Worker}
+   */
+  _addonWorker: null,
+
+  /**
+   * Evaluates code in the sandbox.
+   * @param {String} code
+   *    JavaScript source to evaluate.
+   * @param {String} [filename='javascript:' + code]
+   *    Name of the file
+   */
+  _evaluate: function(code, filename) {
+    try {
+      evaluate(this._sandbox, code, filename || 'javascript:' + code);
+    }
+    catch(e) {
+      this._addonWorker._emit('error', e);
+    }
+  },
+  /**
+   * Imports scripts to the sandbox by reading files under urls and
+   * evaluating its source. If exception occurs during evaluation
+   * `"error"` event is emitted on the worker.
+   * This is actually an analog to the `importScript` method in web
+   * workers but in our case it's not exposed even though content
+   * scripts may be able to do it synchronously since IO operation
+   * takes place in the UI process.
+   */
+  _importScripts: function _importScripts(url) {
+    let urls = Array.slice(arguments, 0);
+    for each (let contentScriptFile in urls) {
+      try {
+        let uri = URL(contentScriptFile);
+        if (uri.scheme === 'resource')
+          load(this._sandbox, String(uri));
+        else
+          throw Error("Unsupported `contentScriptFile` url: " + String(uri));
+      }
+      catch(e) {
+        this._addonWorker._emit('error', e);
+      }
+    }
+  }
+});
+
+/**
+ * Message-passing facility for communication between code running
+ * in the content and add-on process.
+ * @see https://addons.mozilla.org/en-US/developers/docs/sdk/latest/modules/sdk/content/worker.html
+ */
+const Worker = EventEmitter.compose({
+  on: Trait.required,
+  _removeAllListeners: Trait.required,
+
+  // List of messages fired before worker is initialized
+  get _earlyEvents() {
+    delete this._earlyEvents;
+    this._earlyEvents = [];
+    return this._earlyEvents;
+  },
+
+  /**
+   * Sends a message to the worker's global scope. Method takes single
+   * argument, which represents data to be sent to the worker. The data may
+   * be any primitive type value or `JSON`. Call of this method asynchronously
+   * emits `message` event with data value in the global scope of this
+   * symbiont.
+   *
+   * `message` event listeners can be set either by calling
+   * `self.on` with a first argument string `"message"` or by
+   * implementing `onMessage` function in the global scope of this worker.
+   * @param {Number|String|JSON} data
+   */
+  postMessage: function (data) {
+    let args = ['message'].concat(Array.slice(arguments));
+    if (!this._inited) {
+      this._earlyEvents.push(args);
+      return;
+    }
+    processMessage.apply(this, args);
+  },
+
+  /**
+   * EventEmitter, that behaves (calls listeners) asynchronously.
+   * A way to send customized messages to / from the worker.
+   * Events from in the worker can be observed / emitted via
+   * worker.on / worker.emit.
+   */
+  get port() {
+    // We generate dynamically this attribute as it needs to be accessible
+    // before Worker.constructor gets called. (For ex: Panel)
+
+    // create an event emitter that receive and send events from/to the worker
+    this._port = EventEmitterTrait.create({
+      emit: this._emitEventToContent.bind(this)
+    });
+
+    // expose wrapped port, that exposes only public properties:
+    // We need to destroy this getter in order to be able to set the
+    // final value. We need to update only public port attribute as we never
+    // try to access port attribute from private API.
+    delete this._public.port;
+    this._public.port = Cortex(this._port);
+    // Replicate public port to the private object
+    delete this.port;
+    this.port = this._public.port;
+
+    return this._port;
+  },
+
+  /**
+   * Same object than this.port but private API.
+   * Allow access to _emit, in order to send event to port.
+   */
+  _port: null,
+
+  /**
+   * Emit a custom event to the content script,
+   * i.e. emit this event on `self.port`
+   */
+  _emitEventToContent: function () {
+    let args = ['event'].concat(Array.slice(arguments));
+    if (!this._inited) {
+      this._earlyEvents.push(args);
+      return;
+    }
+    processMessage.apply(this, args);
+  },
+
+  // Is worker connected to the content worker sandbox ?
+  _inited: false,
+
+  // Is worker being frozen? i.e related document is frozen in bfcache.
+  // Content script should not be reachable if frozen.
+  _frozen: true,
+
+  constructor: function Worker(options) {
+    options = options || {};
+
+    if ('contentScriptFile' in options)
+      this.contentScriptFile = options.contentScriptFile;
+    if ('contentScriptOptions' in options)
+      this.contentScriptOptions = options.contentScriptOptions;
+    if ('contentScript' in options)
+      this.contentScript = options.contentScript;
+
+    this._setListeners(options);
+
+    unload.ensure(this._public, "destroy");
+
+    // Ensure that worker._port is initialized for contentWorker to be able
+    // to send events during worker initialization.
+    this.port;
+
+    this._documentUnload = this._documentUnload.bind(this);
+    this._pageShow = this._pageShow.bind(this);
+    this._pageHide = this._pageHide.bind(this);
+
+    if ("window" in options) this._attach(options.window);
+  },
+
+  _setListeners: function(options) {
+    if ('onError' in options)
+      this.on('error', options.onError);
+    if ('onMessage' in options)
+      this.on('message', options.onMessage);
+    if ('onDetach' in options)
+      this.on('detach', options.onDetach);
+  },
+
+  _attach: function(window) {
+    this._window = window;
+    // Track document unload to destroy this worker.
+    // We can't watch for unload event on page's window object as it
+    // prevents bfcache from working:
+    // https://developer.mozilla.org/En/Working_with_BFCache
+    this._windowID = getInnerId(this._window);
+    observers.on("inner-window-destroyed", this._documentUnload);
+
+    // Listen to pagehide event in order to freeze the content script
+    // while the document is frozen in bfcache:
+    this._window.addEventListener("pageshow", this._pageShow, true);
+    this._window.addEventListener("pagehide", this._pageHide, true);
+
+    // will set this._contentWorker pointing to the private API:
+    this._contentWorker = WorkerSandbox(this);
+
+    // Mainly enable worker.port.emit to send event to the content worker
+    this._inited = true;
+    this._frozen = false;
+
+    // Process all events and messages that were fired before the
+    // worker was initialized.
+    this._earlyEvents.forEach((function (args) {
+      processMessage.apply(this, args);
+    }).bind(this));
+  },
+
+  _documentUnload: function _documentUnload({ subject, data }) {
+    let innerWinID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+    if (innerWinID != this._windowID) return false;
+    this._workerCleanup();
+    return true;
+  },
+
+  _pageShow: function _pageShow() {
+    this._contentWorker.emitSync("pageshow");
+    this._emit("pageshow");
+    this._frozen = false;
+  },
+
+  _pageHide: function _pageHide() {
+    this._contentWorker.emitSync("pagehide");
+    this._emit("pagehide");
+    this._frozen = true;
+  },
+
+  get url() {
+    // this._window will be null after detach
+    return this._window ? this._window.document.location.href : null;
+  },
+
+  get tab() {
+    // this._window will be null after detach
+    if (this._window)
+      return getTabForWindow(this._window);
+    return null;
+  },
+
+  /**
+   * Tells content worker to unload itself and
+   * removes all the references from itself.
+   */
+  destroy: function destroy() {
+    this._workerCleanup();
+    this._inited = true;
+    this._removeAllListeners();
+  },
+
+  /**
+   * Remove all internal references to the attached document
+   * Tells _port to unload itself and removes all the references from itself.
+   */
+  _workerCleanup: function _workerCleanup() {
+    // maybe unloaded before content side is created
+    // As Symbiont call worker.constructor on document load
+    if (this._contentWorker)
+      this._contentWorker.destroy();
+    this._contentWorker = null;
+    if (this._window) {
+      this._window.removeEventListener("pageshow", this._pageShow, true);
+      this._window.removeEventListener("pagehide", this._pageHide, true);
+    }
+    this._window = null;
+    // This method may be called multiple times,
+    // avoid dispatching `detach` event more than once
+    if (this._windowID) {
+      this._windowID = null;
+      observers.off("inner-window-destroyed", this._documentUnload);
+      this._earlyEvents.length = 0;
+      this._emit("detach");
+    }
+    this._inited = false;
+  },
+
+  /**
+   * Receive an event from the content script that need to be sent to
+   * worker.port. Provide a way for composed object to catch all events.
+   */
+  _onContentScriptEvent: function _onContentScriptEvent() {
+    this._port._emit.apply(this._port, arguments);
+  },
+
+  /**
+   * Reference to the content side of the worker.
+   * @type {WorkerGlobalScope}
+   */
+  _contentWorker: null,
+
+  /**
+   * Reference to the window that is accessible from
+   * the content scripts.
+   * @type {Object}
+   */
+  _window: null,
+
+  /**
+   * Flag to enable `addon` object injection in document. (bug 612726)
+   * @type {Boolean}
+   */
+  _injectInDocument: false
+});
+
+/**
+ * Fired from postMessage and _emitEventToContent, or from the _earlyMessage
+ * queue when fired before the content is loaded. Sends arguments to
+ * contentWorker if able
+ */
+
+function processMessage () {
+  if (!this._contentWorker)
+    throw new Error(ERR_DESTROYED);
+  if (this._frozen)
+    throw new Error(ERR_FROZEN);
+
+  this._contentWorker.emit.apply(null, Array.slice(arguments));
+}
+
+exports.Worker = Worker;
--- a/addon-sdk/source/lib/sdk/event/core.js
+++ b/addon-sdk/source/lib/sdk/event/core.js
@@ -11,16 +11,17 @@ module.metadata = {
 const UNCAUGHT_ERROR = 'An error event was emitted for which there was no listener.';
 const BAD_LISTENER = 'The event listener must be a function.';
 
 const { ns } = require('../core/namespace');
 
 const event = ns();
 
 const EVENT_TYPE_PATTERN = /^on([A-Z]\w+$)/;
+exports.EVENT_TYPE_PATTERN = EVENT_TYPE_PATTERN;
 
 // Utility function to access given event `target` object's event listeners for
 // the specific event `type`. If listeners for this type does not exists they
 // will be created.
 const observers = function observers(target, type) {
   if (!target) throw TypeError("Event target must be an object");
   let listeners = event(target);
   return type in listeners ? listeners[type] : listeners[type] = [];
@@ -156,15 +157,16 @@ exports.count = count;
  *    The type of event.
  * @param {Object} listeners
  *    Dictionary of listeners.
  */
 function setListeners(target, listeners) {
   Object.keys(listeners || {}).forEach(key => {
     let match = EVENT_TYPE_PATTERN.exec(key);
     let type = match && match[1].toLowerCase();
-    let listener = listeners[key];
+    if (!type) return;
 
-    if (type && typeof(listener) === 'function')
+    let listener = listeners[key];
+    if (typeof(listener) === 'function')
       on(target, type, listener);
   });
 }
 exports.setListeners = setListeners;
--- a/addon-sdk/source/lib/sdk/event/utils.js
+++ b/addon-sdk/source/lib/sdk/event/utils.js
@@ -2,17 +2,17 @@
  * 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";
 
 module.metadata = {
   "stability": "unstable"
 };
 
-let { emit, on, once, off } = require("./core");
+let { emit, on, once, off, EVENT_TYPE_PATTERN } = require("./core");
 
 // This module provides set of high order function for working with event
 // streams (streams in a NodeJS style that dispatch data, end and error
 // events).
 
 // Function takes a `target` object and returns set of implicit references
 // (non property references) it keeps. This basically allows defining
 // references between objects without storing the explicitly. See transform for
@@ -250,8 +250,26 @@ Reactor.prototype.onNext = function(pres
 Reactor.prototype.run = function(input) {
   on(input, "data", message => this.onNext(message, input.value));
   on(input, "end", () => this.onEnd(input.value));
   start(input);
   this.value = input.value;
   this.onStart(input.value);
 };
 exports.Reactor = Reactor;
+
+/**
+ * Takes an object used as options with potential keys like 'onMessage',
+ * used to be called `require('sdk/event/core').setListeners` on.
+ * This strips all keys that would trigger a listener to be set.
+ * 
+ * @params {Object} object
+ * @return {Object}
+ */
+
+function stripListeners (object) {
+  return Object.keys(object).reduce((agg, key) => {
+    if (!EVENT_TYPE_PATTERN.test(key))
+      agg[key] = object[key];
+    return agg;
+  }, {});
+}
+exports.stripListeners = stripListeners;
--- a/addon-sdk/source/lib/sdk/l10n/prefs.js
+++ b/addon-sdk/source/lib/sdk/l10n/prefs.js
@@ -1,21 +1,20 @@
 /* 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";
 
-const observers = require("../deprecated/observer-service");
+const { on } = require("../system/events");
 const core = require("./core");
 const { id: jetpackId} = require('../self');
 
 const OPTIONS_DISPLAYED = "addon-options-displayed";
 
-function onOptionsDisplayed(document, addonId) {
+function onOptionsDisplayed({ subjec: document, data: addonId }) {
   if (addonId !== jetpackId)
     return;
   let query = 'setting[data-jetpack-id="' + jetpackId + '"][pref-name], ' +
               'button[data-jetpack-id="' + jetpackId + '"][pref-name]';
   let nodes = document.querySelectorAll(query);
   for (let node of nodes) {
     let name = node.getAttribute("pref-name");
     if (node.tagName == "setting") {
@@ -35,10 +34,9 @@ function onOptionsDisplayed(document, ad
     }
     else if (node.tagName == "button") {
       let label = core.get(name + "_label");
       if (label)
         node.setAttribute("label", label);
     }
   }
 }
-
-observers.add(OPTIONS_DISPLAYED, onOptionsDisplayed);
+on(OPTIONS_DISPLAYED, onOptionsDisplayed);
--- a/addon-sdk/source/lib/sdk/page-mod.js
+++ b/addon-sdk/source/lib/sdk/page-mod.js
@@ -2,17 +2,17 @@
  * 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";
 
 module.metadata = {
   "stability": "stable"
 };
 
-const observers = require('./deprecated/observer-service');
+const observers = require('./system/events');
 const { Loader, validationAttributes } = require('./content/loader');
 const { Worker } = require('./content/worker');
 const { Registry } = require('./util/registry');
 const { EventEmitter } = require('./deprecated/events');
 const { on, emit } = require('./event/core');
 const { validateOptions : validate } = require('./deprecated/api-utils');
 const { Cc, Ci } = require('chrome');
 const { merge } = require('./util/object');
@@ -95,17 +95,17 @@ const PageMod = Loader.compose(EventEmit
                         ' `top` or `frame` value');
     }
     else {
       this.attachTo = ["top", "frame"];
     }
 
     let include = options.include;
     let rules = this.include = Rules();
-    
+
     if (!include)
       throw new Error('The `include` option must always contain atleast one rule');
 
     rules.add.apply(rules, [].concat(include));
 
     if (contentStyle || contentStyleFile) {
       this._style = Style({
         uri: contentStyleFile,
@@ -212,34 +212,34 @@ exports.PageMod = function(options) Page
 exports.PageMod.prototype = PageMod.prototype;
 
 const PageModManager = Registry.resolve({
   constructor: '_init',
   _destructor: '_registryDestructor'
 }).compose({
   constructor: function PageModRegistry(constructor) {
     this._init(PageMod);
-    observers.add(
+    observers.on(
       'document-element-inserted',
       this._onContentWindow = this._onContentWindow.bind(this)
     );
   },
   _destructor: function _destructor() {
-    observers.remove('document-element-inserted', this._onContentWindow);
+    observers.off('document-element-inserted', this._onContentWindow);
     this._removeAllListeners();
 
     // We need to do some cleaning er PageMods, like unregistering any
     // `contentStyle*`
     this._registry.forEach(function(pageMod) {
       pageMod.destroy();
     });
 
     this._registryDestructor();
   },
-  _onContentWindow: function _onContentWindow(document) {
+  _onContentWindow: function _onContentWindow({ subject: document }) {
     let window = document.defaultView;
     // XML documents don't have windows, and we don't yet support them.
     if (!window)
       return;
     // We apply only on documents in tabs of Firefox
     if (!getTabForContentWindow(window))
       return;
 
--- a/addon-sdk/source/lib/sdk/page-worker.js
+++ b/addon-sdk/source/lib/sdk/page-worker.js
@@ -4,18 +4,19 @@
 "use strict";
 
 module.metadata = {
   "stability": "stable"
 };
 
 const { Class } = require('./core/heritage');
 const { on, emit, off, setListeners } = require('./event/core');
-const { filter, pipe, map, merge: streamMerge } = require('./event/utils');
-const { WorkerHost, Worker, detach, attach, destroy } = require('./worker/utils');
+const { filter, pipe, map, merge: streamMerge, stripListeners } = require('./event/utils');
+const { detach, attach, destroy, WorkerHost } = require('./content/utils');
+const { Worker } = require('./content/worker');
 const { Disposable } = require('./core/disposable');
 const { EventTarget } = require('./event/target');
 const { unload } = require('./system/unload');
 const { events, streamEventsFrom } = require('./content/events');
 const { getAttachEventType } = require('./content/utils');
 const { window } = require('./addon/window');
 const { getParentWindow } = require('./window/utils');
 const { create: makeFrame, getDocShell } = require('./frame/utils');
@@ -61,18 +62,18 @@ function enableScript (page) {
 }
 
 function disableScript (page) {
   getDocShell(viewFor(page)).allowJavascript = false;
 }
 
 function Allow (page) {
   return {
-    get script() getDocShell(viewFor(page)).allowJavascript,
-    set script(value) value ? enableScript(page) : disableScript(page)
+    get script() { return getDocShell(viewFor(page)).allowJavascript; },
+    set script(value) { return value ? enableScript(page) : disableScript(page); }
   };
 }
 
 function injectWorker ({page}) {
   let worker = workerFor(page);
   let view = viewFor(page);
   if (isValidURL(page, view.contentDocument.URL))
     attach(worker, view.contentWindow);
@@ -84,42 +85,44 @@ const Page = Class({
   implements: [
     EventTarget,
     Disposable
   ],
   extends: WorkerHost(workerFor),
   setup: function Page(options) {
     let page = this;
     options = pageContract(options);
-    setListeners(this, options);
     let view = makeFrame(window.document, {
       nodeName: 'iframe',
       type: 'content',
       uri: options.contentURL,
       allowJavascript: options.allow.script,
       allowPlugins: true,
       allowAuth: true
     });
 
     ['contentScriptFile', 'contentScript', 'contentScriptWhen']
-      .forEach(function (prop) page[prop] = options[prop]);
+      .forEach(prop => page[prop] = options[prop]);
 
     views.set(this, view);
     pages.set(view, this);
 
-    let worker = new Worker(options);
+    // Set listeners on the {Page} object itself, not the underlying worker,
+    // like `onMessage`, as it gets piped
+    setListeners(this, options);
+    let worker = new Worker(stripListeners(options));
     workers.set(this, worker);
     pipe(worker, this);
 
     if (this.include || options.include) {
       this.rules = Rules();
       this.rules.add.apply(this.rules, [].concat(this.include || options.include));
     }
   },
-  get allow() Allow(this),
+  get allow() { return Allow(this); },
   set allow(value) {
     let allowJavascript = pageContract({ allow: value }).allow.script;
     return allowJavascript ? enableScript(this) : disableScript(this);
   },
   get contentURL() { return viewFor(this).getAttribute('src'); },
   set contentURL(value) {
     if (!isValidURL(this, value)) return;
     let view = viewFor(this);
@@ -128,17 +131,17 @@ const Page = Class({
   },
   dispose: function () {
     if (isDisposed(this)) return;
     let view = viewFor(this);
     if (view.parentNode) view.parentNode.removeChild(view);
     views.delete(this);
     destroy(workers.get(this));
   },
-  toString: function () '[object Page]'
+  toString: function () { return '[object Page]' }
 });
 
 exports.Page = Page;
 
 let pageEvents = streamMerge([events, streamEventsFrom(window)]);
 let readyEvents = filter(pageEvents, isReadyEvent);
 let formattedEvents = map(readyEvents, function({target, type}) {
   return { type: type, page: pageFromDoc(target) };
--- a/addon-sdk/source/lib/sdk/panel.js
+++ b/addon-sdk/source/lib/sdk/panel.js
@@ -14,27 +14,27 @@ module.metadata = {
 
 const { Ci } = require("chrome");
 const { validateOptions: valid } = require('./deprecated/api-utils');
 const { setTimeout } = require('./timers');
 const { isPrivateBrowsingSupported } = require('./self');
 const { isWindowPBSupported } = require('./private-browsing/utils');
 const { Class } = require("./core/heritage");
 const { merge } = require("./util/object");
-const { WorkerHost, Worker, detach, attach, destroy,
-        requiresAddonGlobal } = require("./worker/utils");
+const { WorkerHost, detach, attach, destroy } = require("./content/utils");
+const { Worker } = require("./content/worker");
 const { Disposable } = require("./core/disposable");
 const { contract: loaderContract } = require("./content/loader");
 const { contract } = require("./util/contract");
 const { on, off, emit, setListeners } = require("./event/core");
 const { EventTarget } = require("./event/target");
 const domPanel = require("./panel/utils");
 const { events } = require("./panel/events");
 const systemEvents = require("./system/events");
-const { filter, pipe } = require("./event/utils");
+const { filter, pipe, stripListeners } = require("./event/utils");
 const { getNodeView, getActiveView } = require("./view/core");
 const { isNil, isObject } = require("./lang/type");
 const { getAttachEventType } = require("./content/utils");
 
 let number = { is: ['number', 'undefined', 'null'] };
 let boolean = { is: ['boolean', 'undefined', 'null'] };
 
 let rectContract = contract({
@@ -112,30 +112,30 @@ const Panel = Class({
     let model = merge({
       defaultWidth: 320,
       defaultHeight: 240,
       focus: true,
       position: Object.freeze({}),
     }, panelContract(options));
     models.set(this, model);
 
-    // Setup listeners.
-    setListeners(this, options);
 
     // Setup view
     let view = domPanel.make();
     panels.set(view, this);
     views.set(this, view);
 
     // Load panel content.
     domPanel.setURL(view, model.contentURL);
 
     setupAutoHide(this);
 
-    let worker = new Worker(options);
+    // Setup listeners.
+    setListeners(this, options);
+    let worker = new Worker(stripListeners(options));
     workers.set(this, worker);
 
     // pipe events from worker to a panel.
     pipe(worker, this);
   },
   dispose: function dispose() {
     this.hide();
     off(this);
--- a/addon-sdk/source/lib/sdk/simple-prefs.js
+++ b/addon-sdk/source/lib/sdk/simple-prefs.js
@@ -3,30 +3,24 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
 module.metadata = {
   "stability": "experimental"
 };
 
 const { emit, off } = require("./event/core");
-const { when: unload } = require("./system/unload");
 const { PrefsTarget } = require("./preferences/event-target");
 const { id } = require("./self");
-const observers = require("./deprecated/observer-service");
+const { on } = require("./system/events");
 
 const ADDON_BRANCH = "extensions." + id + ".";
 const BUTTON_PRESSED = id + "-cmdPressed";
 
 const target = PrefsTarget({ branchName: ADDON_BRANCH });
 
 // Listen to clicks on buttons
-function buttonClick(subject, data) {
+function buttonClick({ data }) {
   emit(target, data);
 }
-observers.add(BUTTON_PRESSED, buttonClick);
-
-// Make sure we cleanup listeners on unload.
-unload(function() {
-  observers.remove(BUTTON_PRESSED, buttonClick);
-});
+on(BUTTON_PRESSED, buttonClick);
 
 module.exports = target;
--- a/addon-sdk/source/lib/sdk/test/harness.js
+++ b/addon-sdk/source/lib/sdk/test/harness.js
@@ -1,33 +1,40 @@
 /* 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";
 
 module.metadata = {
   "stability": "experimental"
 };
 
 const { Cc, Ci, Cu } = require("chrome");
 const { Loader } = require('./loader');
 const { serializeStack, parseStack  } = require("toolkit/loader");
 const { setTimeout } = require('../timers');
-const memory = require('../deprecated/memory');
 const { PlainTextConsole } = require("../console/plain-text");
 const { when: unload } = require("../system/unload");
 const { format, fromException }  = require("../console/traceback");
 const system = require("../system");
+const memory = require('../deprecated/memory');
+const { gc: gcPromise } = require('./memory');
+const { defer } = require('../core/promise');
 
 // Trick manifest builder to make it think we need these modules ?
 const unit = require("../deprecated/unit-test");
 const test = require("../../test");
 const url = require("../url");
 
+function emptyPromise() {
+  let { promise, resolve } = defer();
+  resolve();
+  return promise;
+}
+
 var cService = Cc['@mozilla.org/consoleservice;1'].getService()
                .QueryInterface(Ci.nsIConsoleService);
 
 // The console used to log messages
 var testConsole;
 
 // Cuddlefish loader in which we load and execute tests.
 var loader;
@@ -138,67 +145,61 @@ function dictDiff(last, curr) {
     var result = curr[name] - (last[name] || 0);
     if (result)
       diff[name] = (result > 0 ? "+" : "") + result;
   }
   return diff;
 }
 
 function reportMemoryUsage() {
-  memory.gc();
-
-  var mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
-            .getService(Ci.nsIMemoryReporterManager);
-
-  // Bug 916501: this code is *so* bogus -- nsIMemoryReporter changed its |memoryUsed|
-  // field to |amount| *years* ago, and even bigger changes have happened
-  // since -- that it must just never be run.
-  var reporters = mgr.enumerateReporters();
-  if (reporters.hasMoreElements())
-    print("\n");
-
-  while (reporters.hasMoreElements()) {
-    var reporter = reporters.getNext();
-    reporter.QueryInterface(Ci.nsIMemoryReporter);
-    print(reporter.description + ": " + reporter.memoryUsed + "\n");
+  if (!profileMemory) {
+    return emptyPromise();
   }
 
-  var weakrefs = [info.weakref.get()
-                  for each (info in memory.getObjects())];
-  weakrefs = [weakref for each (weakref in weakrefs) if (weakref)];
-  print("Tracked memory objects in testing sandbox: " +
-        weakrefs.length + "\n");
+  return gcPromise().then((function () {
+    var mgr = Cc["@mozilla.org/memory-reporter-manager;1"]
+              .getService(Ci.nsIMemoryReporterManager);
+    let count = 0;
+    function logReporter(process, path, kind, units, amount, description) {
+      print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n");
+    }
+    mgr.getReportsForThisProcess(logReporter, null);
+
+    var weakrefs = [info.weakref.get()
+                    for each (info in memory.getObjects())];
+    weakrefs = [weakref for each (weakref in weakrefs) if (weakref)];
+    print("Tracked memory objects in testing sandbox: " + weakrefs.length + "\n");
+  }));
 }
 
 var gWeakrefInfo;
 
 function checkMemory() {
-  memory.gc();
-  Cu.schedulePreciseGC(function () {
+  return gcPromise().then(_ => {
     let leaks = getPotentialLeaks();
 
     let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) {
       return !(url in startLeaks.compartments);
     });
 
     let windowURLs = Object.keys(leaks.windows).filter(function(url) {
       return !(url in startLeaks.windows);
     });
 
     for (let url of compartmentURLs)
       console.warn("LEAKED", leaks.compartments[url]);
 
     for (let url of windowURLs)
       console.warn("LEAKED", leaks.windows[url]);
-
-    showResults();
-  });
+  }).then(showResults);
 }
 
 function showResults() {
+  let { promise, resolve } = defer();
+
   if (gWeakrefInfo) {
     gWeakrefInfo.forEach(
       function(info) {
         var ref = info.weakref.get();
         if (ref !== null) {
           var data = ref.__url__ ? ref.__url__ : ref;
           var warning = data == "[object Object]"
             ? "[object " + data.constructor.name + "(" +
@@ -206,16 +207,19 @@ function showResults() {
             : data;
           console.warn("LEAK", warning, info.bin);
         }
       }
     );
   }
 
   onDone(results);
+
+  resolve();
+  return promise;
 }
 
 function cleanup() {
   let coverObject = {};
   try {
     for (let name in loader.modules)
       memory.track(loader.modules[name],
                            "module global scope: " + name);
@@ -245,17 +249,18 @@ function cleanup() {
     if (typeof loader.globals.global == "object") {
       coverObject = loader.globals.global['__$coverObject'] || {};
     }
 
     consoleListener.errorsLogged = 0;
     loader = null;
 
     memory.gc();
-  } catch (e) {
+  }
+  catch (e) {
     results.failed++;
     console.error("unload.send() threw an exception.");
     console.exception(e);
   };
 
   setTimeout(require('@test/options').checkMemory ? checkMemory : showResults, 1);
 
   // dump the coverobject
@@ -328,34 +333,33 @@ function getPotentialLeaks() {
       if (matches[1] in compartments)
         return;
 
       let details = compartmentDetails.exec(matches[1]);
       if (!details) {
         console.error("Unable to parse compartment detail " + matches[1]);
         return;
       }
- 
+
       let item = {
         path: matches[1],
         principal: details[1],
         location: details[2] ? details[2].replace("\\", "/", "g") : undefined,
         source: details[3] ? details[3].split(" -> ").reverse() : undefined,
         toString: function() this.location
       };
 
       if (!isPossibleLeak(item))
         return;
 
       compartments[matches[1]] = item;
       return;
     }
 
-    matches = windowRegexp.exec(path);
-    if (matches) {
+    if (matches = windowRegexp.exec(path)) {
       if (matches[1] in windows)
         return;
 
       let details = windowDetails.exec(matches[1]);
       if (!details) {
         console.error("Unable to parse window detail " + matches[1]);
         return;
       }
@@ -369,45 +373,50 @@ function getPotentialLeaks() {
 
       if (!isPossibleLeak(item))
         return;
 
       windows[matches[1]] = item;
     }
   }
 
-  let mgr = Cc["@mozilla.org/memory-reporter-manager;1"].
-            getService(Ci.nsIMemoryReporterManager);
-
-  mgr.getReportsForThisProcess(logReporter, null);
+  Cc["@mozilla.org/memory-reporter-manager;1"]
+    .getService(Ci.nsIMemoryReporterManager)
+    .getReportsForThisProcess(logReporter, null);
 
   return { compartments: compartments, windows: windows };
 }
 
 function nextIteration(tests) {
   if (tests) {
     results.passed += tests.passed;
     results.failed += tests.failed;
 
-    if (profileMemory)
-      reportMemoryUsage();
+    reportMemoryUsage().then(_ => {
+      let testRun = [];
+      for each (let test in tests.testRunSummary) {
+        let testCopy = {};
+        for (let info in test) {
+          testCopy[info] = test[info];
+        }
+        testRun.push(testCopy);
+      }
 
-    let testRun = [];
-    for each (let test in tests.testRunSummary) {
-      let testCopy = {};
-      for (let info in test) {
-        testCopy[info] = test[info];
-      }
-      testRun.push(testCopy);
-    }
+      results.testRuns.push(testRun);
+      iterationsLeft--;
 
-    results.testRuns.push(testRun);
-    iterationsLeft--;
+      checkForEnd();
+    })
   }
+  else {
+    checkForEnd();
+  }
+}
 
+function checkForEnd() {
   if (iterationsLeft && (!stopOnError || results.failed == 0)) {
     // Pass the loader which has a hooked console that doesn't dispatch
     // errors to the JS console and avoid firing false alarm in our
     // console listener
     findAndRunTests(loader, nextIteration);
   }
   else {
     setTimeout(cleanup, 0);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/lib/sdk/test/memory.js
@@ -0,0 +1,17 @@
+'use strict';
+
+const { Cu } = require("chrome");
+const memory = require('../deprecated/memory');
+const { defer } = require('../core/promise');
+
+function gc() {
+  let { promise, resolve } = defer();
+
+  Cu.forceGC();
+  memory.gc();
+
+  Cu.schedulePreciseGC(_ => resolve());
+
+  return promise;
+}
+exports.gc = gc;
--- a/addon-sdk/source/lib/sdk/ui/button/action.js
+++ b/addon-sdk/source/lib/sdk/ui/button/action.js
@@ -5,16 +5,27 @@
 
 module.metadata = {
   'stability': 'experimental',
   'engines': {
     'Firefox': '> 28'
   }
 };
 
+// Because Firefox Holly, we still need to check if `CustomizableUI` is
+// available. Once Australis will officially land, we can safely remove it.
+// See Bug 959142
+try {
+  require('chrome').Cu.import('resource:///modules/CustomizableUI.jsm', {});
+}
+catch (e) {
+  throw Error('Unsupported Application: The module ' + module.id +
+              ' does not support this application.');
+}
+
 const { Class } = require('../../core/heritage');
 const { merge } = require('../../util/object');
 const { Disposable } = require('../../core/disposable');
 const { on, off, emit, setListeners } = require('../../event/core');
 const { EventTarget } = require('../../event/target');
 
 const view = require('./view');
 const { buttonContract, stateContract } = require('./contract');
--- a/addon-sdk/source/lib/sdk/ui/button/toggle.js
+++ b/addon-sdk/source/lib/sdk/ui/button/toggle.js
@@ -5,16 +5,27 @@
 
 module.metadata = {
   'stability': 'experimental',
   'engines': {
     'Firefox': '> 28'
   }
 };
 
+// Because Firefox Holly, we still need to check if `CustomizableUI` is
+// available. Once Australis will officially land, we can safely remove it.
+// See Bug 959142
+try {
+  require('chrome').Cu.import('resource:///modules/CustomizableUI.jsm', {});
+}
+catch (e) {
+  throw Error('Unsupported Application: The module ' + module.id +
+              ' does not support this application.');
+}
+
 const { Class } = require('../../core/heritage');
 const { merge } = require('../../util/object');
 const { Disposable } = require('../../core/disposable');
 const { on, off, emit, setListeners } = require('../../event/core');
 const { EventTarget } = require('../../event/target');
 
 const view = require('./view');
 const { toggleButtonContract, toggleStateContract } = require('./contract');
--- a/addon-sdk/source/lib/sdk/ui/button/view.js
+++ b/addon-sdk/source/lib/sdk/ui/button/view.js
@@ -9,17 +9,18 @@ module.metadata = {
     'Firefox': '> 28'
   }
 };
 
 const { Cu } = require('chrome');
 const { on, off, emit } = require('../../event/core');
 
 const { id: addonID, data } = require('sdk/self');
-const buttonPrefix = 'button--' + addonID.replace(/@/g, '-at-');
+const buttonPrefix =
+  'button--' + addonID.toLowerCase().replace(/[^a-z0-9-_]/g, '');
 
 const { isObject } = require('../../lang/type');
 
 const { getMostRecentBrowserWindow } = require('../../window/utils');
 const { ignoreWindow } = require('../../private-browsing/utils');
 const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
 const { AREA_PANEL, AREA_NAVBAR } = CustomizableUI;
 
--- a/addon-sdk/source/lib/sdk/ui/frame.js
+++ b/addon-sdk/source/lib/sdk/ui/frame.js
@@ -5,12 +5,23 @@
 
 module.metadata = {
   "stability": "experimental",
   "engines": {
     "Firefox": "> 28"
   }
 };
 
+// Because Firefox Holly, we still need to check if `CustomizableUI` is
+// available. Once Australis will officially land, we can safely remove it.
+// See Bug 959142
+try {
+  require("chrome").Cu.import("resource:///modules/CustomizableUI.jsm", {});
+}
+catch (e) {
+  throw Error("Unsupported Application: The module"  + module.id +
+              " does not support this application.");
+}
+
 require("./frame/view");
 const { Frame } = require("./frame/model");
 
 exports.Frame = Frame;
--- a/addon-sdk/source/lib/sdk/ui/sidebar.js
+++ b/addon-sdk/source/lib/sdk/ui/sidebar.js
@@ -19,32 +19,26 @@ const { URL } = require('../url');
 const { add, remove, has, clear, iterator } = require('../lang/weak-set');
 const { id: addonID } = require('../self');
 const { WindowTracker } = require('../deprecated/window-utils');
 const { isShowing } = require('./sidebar/utils');
 const { isBrowser, getMostRecentBrowserWindow, windows, isWindowPrivate } = require('../window/utils');
 const { ns } = require('../core/namespace');
 const { remove: removeFromArray } = require('../util/array');
 const { show, hide, toggle } = require('./sidebar/actions');
-const { Worker: WorkerTrait } = require('../content/worker');
+const { Worker } = require('../content/worker');
 const { contract: sidebarContract } = require('./sidebar/contract');
 const { create, dispose, updateTitle, updateURL, isSidebarShowing, showSidebar, hideSidebar } = require('./sidebar/view');
 const { defer } = require('../core/promise');
 const { models, views, viewsFor, modelFor } = require('./sidebar/namespace');
 const { isLocalURL } = require('../url');
 const { ensure } = require('../system/unload');
 const { identify } = require('./id');
 const { uuid } = require('../util/uuid');
 
-const Worker = WorkerTrait.resolve({
-  _injectInDocument: '__injectInDocument'
-}).compose({
-  get _injectInDocument() true
-});
-
 const sidebarNS = ns();
 
 const WEB_PANEL_BROWSER_ID = 'web-panels-browser';
 
 let sidebars = {};
 
 const Sidebar = Class({
   implements: [ Disposable ],
@@ -113,17 +107,18 @@ const Sidebar = Class({
           let sbTitle = window.document.getElementById('sidebar-title');
           function onWebPanelSidebarCreated() {
             if (panelBrowser.contentWindow.location != model.url ||
                 sbTitle.value != model.title) {
               return;
             }
 
             let worker = windowNS(window).worker = Worker({
-              window: panelBrowser.contentWindow
+              window: panelBrowser.contentWindow,
+              injectInDocument: true
             });
 
             function onWebPanelSidebarUnload() {
               windowNS(window).onWebPanelSidebarUnload = null;
 
               // uncheck the associated menuitem
               bar.setAttribute('checked', 'false');
 
--- a/addon-sdk/source/lib/sdk/ui/toolbar.js
+++ b/addon-sdk/source/lib/sdk/ui/toolbar.js
@@ -5,12 +5,23 @@
 
 module.metadata = {
   "stability": "experimental",
   "engines": {
     "Firefox": "> 28"
   }
 };
 
+// Because Firefox Holly, we still need to check if `CustomizableUI` is
+// available. Once Australis will officially land, we can safely remove it.
+// See Bug 959142
+try {
+  require("chrome").Cu.import("resource:///modules/CustomizableUI.jsm", {});
+}
+catch (e) {
+  throw Error("Unsupported Application: The module"  + module.id +
+              " does not support this application.");
+}
+
 const { Toolbar } = require("./toolbar/model");
 require("./toolbar/view");
 
 exports.Toolbar = Toolbar;
--- a/addon-sdk/source/lib/sdk/window/utils.js
+++ b/addon-sdk/source/lib/sdk/window/utils.js
@@ -4,17 +4,16 @@
 'use strict';
 
 module.metadata = {
   'stability': 'unstable'
 };
 
 const { Cc, Ci } = require('chrome');
 const array = require('../util/array');
-const observers = require('../deprecated/observer-service');
 const { defer } = require('sdk/core/promise');
 
 const windowWatcher = Cc['@mozilla.org/embedcomp/window-watcher;1'].
                        getService(Ci.nsIWindowWatcher);
 const appShellService = Cc['@mozilla.org/appshell/appShellService;1'].
                         getService(Ci.nsIAppShellService);
 const WM = Cc['@mozilla.org/appshell/window-mediator;1'].
            getService(Ci.nsIWindowMediator);
@@ -147,35 +146,16 @@ function getWindowLoadingContext(window)
          QueryInterface(Ci.nsILoadContext);
 }
 exports.getWindowLoadingContext = getWindowLoadingContext;
 
 const isTopLevel = window => window && getToplevelWindow(window) === window;
 exports.isTopLevel = isTopLevel;
 
 /**
- * Removes given window from the application's window registry. Unless
- * `options.close` is `false` window is automatically closed on application
- * quit.
- * @params {nsIDOMWindow} window
- * @params {Boolean} options.close
- */
-function backgroundify(window, options) {
-  let base = getBaseWindow(window);
-  base.visibility = false;
-  base.enabled = false;
-  appShellService.unregisterTopLevelWindow(getXULWindow(window));
-  if (!options || options.close !== false)
-    observers.add('quit-application-granted', window.close.bind(window));
-
-  return window;
-}
-exports.backgroundify = backgroundify;
-
-/**
  * Takes hash of options and serializes it to a features string that
  * can be used passed to `window.open`. For more details on features string see:
  * https://developer.mozilla.org/en/DOM/window.open#Position_and_size_features
  */
 function serializeFeatures(options) {
   return Object.keys(options).reduce(function(result, name) {
     let value = options[name];
 
--- a/addon-sdk/source/lib/sdk/worker/utils.js
+++ b/addon-sdk/source/lib/sdk/worker/utils.js
@@ -1,103 +1,19 @@
 /* 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";
+'use strict';
 
 module.metadata = {
-  "stability": "unstable"
+  'stability': 'deprecated'
 };
 
-// This module attempts to hide trait based nature of the worker so that
-// code depending on workers could be de-trait-ified without changing worker
-// implementation.
-
-const { Worker: WorkerTrait } = require("../content/worker");
-const { Loader } = require("../content/loader");
-const { merge } = require("../util/object");
-const { emit } = require("../event/core");
-
-let assetsURI = require("../self").data.url();
-let isArray = Array.isArray;
-
-function isAddonContent({ contentURL }) {
-  return typeof(contentURL) === "string" && contentURL.indexOf(assetsURI) === 0;
-}
-
-function hasContentScript({ contentScript, contentScriptFile }) {
-  return (isArray(contentScript) ? contentScript.length > 0 :
-         !!contentScript) ||
-         (isArray(contentScriptFile) ? contentScriptFile.length > 0 :
-         !!contentScriptFile);
-}
-
-function requiresAddonGlobal(model) {
-  return isAddonContent(model) && !hasContentScript(model);
-}
-exports.requiresAddonGlobal = requiresAddonGlobal;
-
-
-const LegacyWorker = WorkerTrait.compose(Loader).resolve({
-  _setListeners: "__setListeners",
-  _injectInDocument: "__injectInDocument",
-  contentURL: "__contentURL"
-}).compose({
-  _setListeners: function() {},
-  get contentURL() this._window.document.URL,
-  get _injectInDocument() requiresAddonGlobal(this),
-  attach: function(window) this._attach(window),
-  detach: function() this._workerCleanup()
-});
-
-// Weak map that stores mapping between regular worker instances and
-// legacy trait based worker instances.
-let traits = new WeakMap();
-
-function traitFor(worker) traits.get(worker, null);
+const {
+  requiresAddonGlobal, attach, detach, destroy, WorkerHost
+} = require('../content/utils');
 
-function WorkerHost(workerFor) {
-  // Define worker properties that just proxy to a wrapped trait.
-  return ["postMessage", "port", "url", "tab"].reduce(function(proto, name) {
-    Object.defineProperty(proto, name, {
-      enumerable: true,
-      configurable: false,
-      get: function() traitFor(workerFor(this))[name],
-      set: function(value) traitFor(workerFor(this))[name] = value
-    });
-    return proto;
-  }, {});
-}
 exports.WorkerHost = WorkerHost;
-
-// Type representing worker instance.
-function Worker(options) {
-  let worker = Object.create(Worker.prototype);
-  let trait = new LegacyWorker(options);
-  ["pageshow", "pagehide", "detach", "message", "error"].forEach(function(key) {
-    trait.on(key, function() {
-      emit.apply(emit, [worker, key].concat(Array.slice(arguments)));
-    });
-  });
-  traits.set(worker, trait);
-  return worker;
-}
-exports.Worker = Worker;
-
-function detach(worker) {
-  let trait = traitFor(worker);
-  if (trait) trait.detach();
-}
 exports.detach = detach;
-
-function attach(worker, window) {
-  let trait = traitFor(worker);
-  // Cleanup the worker before injecting the content script into a new document.
-  trait.attach(window);
-}
 exports.attach = attach;
-
-function destroy(worker) {
-  let trait = traitFor(worker);
-  if (trait) trait.destroy();
-}
 exports.destroy = destroy;
+exports.requiresAddonGlobal = requiresAddonGlobal;
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -608,18 +608,30 @@ const Require = iced(function Require(lo
           throw err;
         uri = uri + '.js';
       }
     }
     // If not yet cached, load and cache it.
     // We also freeze module to prevent it from further changes
     // at runtime.
     if (!(uri in modules)) {
+      // Many of the loader's functionalities are dependent
+      // on modules[uri] being set before loading, so we set it and 
+      // remove it if we have any errors.
       module = modules[uri] = Module(requirement, uri);
-      freeze(load(loader, module));
+      try {
+        freeze(load(loader, module));
+      }
+      catch (e) {
+        // Clear out modules cache so we can throw on a second invalid require
+        delete modules[uri];
+        // Also clear out the Sandbox that was created
+        delete loader.sandboxes[uri];
+        throw e;
+      }
     }
 
     return module.exports;
   }
   // Make `require.main === module` evaluate to true in main module scope.
   require.main = loader.main === requirer ? requirer : undefined;
   return iced(require);
 });
--- a/addon-sdk/source/mapping.json
+++ b/addon-sdk/source/mapping.json
@@ -10,17 +10,16 @@
   "l10n/html": "sdk/l10n/html",
   "l10n/loader": "sdk/l10n/loader",
   "l10n/locale": "sdk/l10n/locale",
   "l10n/prefs": "sdk/l10n/prefs",
   "list": "sdk/util/list",
   "loader": "sdk/loader/loader",
   "memory": "sdk/deprecated/memory",
   "namespace": "sdk/core/namespace",
-  "observer-service": "sdk/deprecated/observer-service",
   "preferences-service": "sdk/preferences/service",
   "promise": "sdk/core/promise",
   "system": "sdk/system",
   "system/events": "sdk/system/events",
   "tabs/tab": "sdk/tabs/tab",
   "tabs/utils": "sdk/tabs/utils",
   "timer": "sdk/timers",
   "traits": "sdk/deprecated/traits",
--- a/addon-sdk/source/test/addons/layout-change/main.js
+++ b/addon-sdk/source/test/addons/layout-change/main.js
@@ -91,19 +91,16 @@ exports["test compatibility"] = function
                require("sdk/page-worker"), "sdk/page-worker -> page-worker");
 
   assert.equal(require("timer"),
                require("sdk/timers"), "sdk/timers -> timer");
 
   assert.equal(require("xhr"),
                require("sdk/net/xhr"), "sdk/io/xhr -> xhr");
 
-  assert.equal(require("observer-service"),
-               require("sdk/deprecated/observer-service"), "sdk/deprecated/observer-service -> observer-service");
-
   assert.equal(require("private-browsing"),
                require("sdk/private-browsing"), "sdk/private-browsing -> private-browsing");
 
   assert.equal(require("passwords"),
                require("sdk/passwords"), "sdk/passwords -> passwords");
 
   assert.equal(require("events"),
                require("sdk/deprecated/events"), "sdk/deprecated/events -> events");
@@ -142,19 +139,16 @@ exports["test compatibility"] = function
                require("sdk/querystring"), "sdk/querystring -> querystring");
 
   assert.equal(loader.require("addon-page"),
                loader.require("sdk/addon-page"), "sdk/addon-page -> addon-page");
 
   assert.equal(require("tabs/utils"),
                require("sdk/tabs/utils"), "sdk/tabs/utils -> tabs/utils");
 
-  assert.equal(require("app-strings"),
-               require("sdk/deprecated/app-strings"), "sdk/deprecated/app-strings -> app-strings");
-
   assert.equal(require("dom/events"),
                require("sdk/dom/events"), "sdk/dom/events -> dom/events");
 
   assert.equal(require("tabs/tab.js"),
                require("sdk/tabs/tab"), "sdk/tabs/tab -> tabs/tab.js");
 
   assert.equal(require("memory"),
                require("sdk/deprecated/memory"), "sdk/deprecated/memory -> memory");
--- a/addon-sdk/source/test/addons/symbiont/main.js
+++ b/addon-sdk/source/test/addons/symbiont/main.js
@@ -1,16 +1,16 @@
 /* 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";
 
 const { data } = require("sdk/self");
-const { Symbiont } = require("sdk/content/symbiont");
+const { Symbiont } = require("sdk/deprecated/symbiont");
 
 exports["test:direct communication with trusted document"] = function(assert, done) {
   let worker = Symbiont({
     contentURL: data.url("test-trusted-document.html")
   });
 
   worker.port.on('document-to-addon', function (arg) {
     assert.equal(arg, "ok", "Received an event from the document");
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/loader/missing-twice/file.json
@@ -0,0 +1,1 @@
+an invalid json file
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/loader/missing-twice/main.js
@@ -0,0 +1,32 @@
+/* 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';
+
+try {
+  require('./not-found');
+}
+catch (e1) {
+  exports.firstError = e1;
+  // It should throw again and not be cached
+  try {
+    require('./not-found');
+  }
+  catch (e2) {
+    exports.secondError = e2;
+  }
+}
+
+try {
+  require('./file.json');
+}
+catch (e) {
+  exports.invalidJSON1 = e;
+  try {
+    require('./file.json');
+  }
+  catch (e) {
+    exports.invalidJSON2 = e;
+  }
+}
--- a/addon-sdk/source/test/tabs/test-firefox-tabs.js
+++ b/addon-sdk/source/test/tabs/test-firefox-tabs.js
@@ -4,33 +4,36 @@
 'use strict';
 
 const { Cc, Ci } = require('chrome');
 const { Loader } = require('sdk/test/loader');
 const timer = require('sdk/timers');
 const { getOwnerWindow } = require('sdk/private-browsing/window/utils');
 const { windows, onFocus, getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { open, focus, close } = require('sdk/window/helpers');
-const { StringBundle } = require('sdk/deprecated/app-strings');
 const tabs = require('sdk/tabs');
 const { browserWindows } = require('sdk/windows');
 const { set: setPref } = require("sdk/preferences/service");
 const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings";
 
 const base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAA";
 
 // Bug 682681 - tab.title should never be empty
 exports.testBug682681_aboutURI = function(assert, done) {
-  let tabStrings = StringBundle('chrome://browser/locale/tabbrowser.properties');
+  let url = 'chrome://browser/locale/tabbrowser.properties';
+  let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
+                        getService(Ci.nsIStringBundleService).
+                        createBundle(url);
+  let emptyTabTitle = stringBundle.GetStringFromName('tabs.emptyTabTitle');
 
   tabs.on('ready', function onReady(tab) {
     tabs.removeListener('ready', onReady);
 
     assert.equal(tab.title,
-                     tabStrings.get('tabs.emptyTabTitle'),
+                     emptyTabTitle,
                      "title of about: tab is not blank");
 
     tab.close(done);
   });
 
   // open a about: url
   tabs.open({
     url: "about:blank",
--- a/addon-sdk/source/test/test-addon-installer.js
+++ b/addon-sdk/source/test/test-addon-installer.js
@@ -1,56 +1,55 @@
 /* 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";
 
 const { Cc, Ci, Cu } = require("chrome");
 const AddonInstaller = require("sdk/addon/installer");
-const observers = require("sdk/deprecated/observer-service");
+const { on, off } = require("sdk/system/events");
 const { setTimeout } = require("sdk/timers");
 const tmp = require("sdk/test/tmp-file");
 const system = require("sdk/system");
 const fixtures = require("./fixtures");
 
 const testFolderURL = module.uri.split('test-addon-installer.js')[0];
 const ADDON_URL = testFolderURL + "fixtures/addon-install-unit-test@mozilla.com.xpi";
 const ADDON_PATH = tmp.createFromURL(ADDON_URL);
 
 exports["test Install"] = function (assert, done) {
 
   // Save all events distpatched by bootstrap.js of the installed addon
   let events = [];
-  function eventsObserver(subject, data) {
+  function eventsObserver({ data }) {
     events.push(data);
   }
-  observers.add("addon-install-unit-test", eventsObserver, false);
+  on("addon-install-unit-test", eventsObserver);
 
   // Install the test addon
   AddonInstaller.install(ADDON_PATH).then(
     function onInstalled(id) {
       assert.equal(id, "addon-install-unit-test@mozilla.com", "`id` is valid");
 
       // Now uninstall it
       AddonInstaller.uninstall(id).then(function () {
         // Ensure that bootstrap.js methods of the addon have been called
         // successfully and in the right order
         let expectedEvents = ["install", "startup", "shutdown", "uninstall"];
         assert.equal(JSON.stringify(events),
                          JSON.stringify(expectedEvents),
                          "addon's bootstrap.js functions have been called");
 
-        observers.remove("addon-install-unit-test", eventsObserver);
+        off("addon-install-unit-test", eventsObserver);
         done();
       });
     },
     function onFailure(code) {
       assert.fail("Install failed: "+code);
-      observers.remove("addon-install-unit-test", eventsObserver);
+      off("addon-install-unit-test", eventsObserver);
       done();
     }
   );
 };
 
 exports["test Failing Install With Invalid Path"] = function (assert, done) {
   AddonInstaller.install("invalid-path").then(
     function onInstalled(id) {
@@ -79,20 +78,18 @@ exports["test Failing Install With Inval
     }
   );
 }
 
 exports["test Update"] = function (assert, done) {
   // Save all events distpatched by bootstrap.js of the installed addon
   let events = [];
   let iteration = 1;
-  function eventsObserver(subject, data) {
-    events.push(data);
-  }
-  observers.add("addon-install-unit-test", eventsObserver);
+  let eventsObserver = ({data}) => events.push(data);
+  on("addon-install-unit-test", eventsObserver);
 
   function onInstalled(id) {
     let prefix = "[" + iteration + "] ";
     assert.equal(id, "addon-install-unit-test@mozilla.com",
                      prefix + "`id` is valid");
 
     // On 2nd and 3rd iteration, we receive uninstall events from the last
     // previously installed addon
@@ -110,24 +107,24 @@ exports["test Update"] = function (asser
     else {
       events = [];
       AddonInstaller.uninstall(id).then(function() {
         let expectedEvents = ["shutdown", "uninstall"];
         assert.equal(JSON.stringify(events),
                      JSON.stringify(expectedEvents),
                      prefix + "addon's bootstrap.js functions have been called");
 
-        observers.remove("addon-install-unit-test", eventsObserver);
+        off("addon-install-unit-test", eventsObserver);
         done();
       });
     }
   }
   function onFailure(code) {
     assert.fail("Install failed: "+code);
-    observers.remove("addon-install-unit-test", eventsObserver);
+    off("addon-install-unit-test", eventsObserver);
     done();
   }
 
   function next() {
     events = [];
     AddonInstaller.install(ADDON_PATH).then(onInstalled, onFailure);
   }
 
--- a/addon-sdk/source/test/test-api-utils.js
+++ b/addon-sdk/source/test/test-api-utils.js
@@ -1,39 +1,14 @@
 /* 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/. */
 
 const apiUtils = require("sdk/deprecated/api-utils");
 
-exports.testPublicConstructor = function (assert) {
-  function PrivateCtor() {}
-  PrivateCtor.prototype = {};
-
-  let PublicCtor = apiUtils.publicConstructor(PrivateCtor);
-  assert.ok(
-    PrivateCtor.prototype.isPrototypeOf(PublicCtor.prototype),
-    "PrivateCtor.prototype should be prototype of PublicCtor.prototype"
-  );
-
-  function testObj(useNew) {
-    let obj = useNew ? new PublicCtor() : PublicCtor();
-    assert.ok(obj instanceof PublicCtor,
-                "Object should be instance of PublicCtor");
-    assert.ok(obj instanceof PrivateCtor,
-                "Object should be instance of PrivateCtor");
-    assert.ok(PublicCtor.prototype.isPrototypeOf(obj),
-                "PublicCtor's prototype should be prototype of object");
-    assert.equal(obj.constructor, PublicCtor,
-                     "Object constructor should be PublicCtor");
-  }
-  testObj(true);
-  testObj(false);
-};
-
 exports.testValidateOptionsEmpty = function (assert) {
   let val = apiUtils.validateOptions(null, {});
 
   assert.deepEqual(val, {});
 
   val = apiUtils.validateOptions(null, { foo: {} });
   assert.deepEqual(val, {});
 
deleted file mode 100644
--- a/addon-sdk/source/test/test-app-strings.js
+++ /dev/null
@@ -1,68 +0,0 @@
-/* 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";
-
-const { Cc, Ci } = require("chrome");
-const { StringBundle } = require("sdk/deprecated/app-strings");
-
-exports.testStringBundle = function(assert) {
-  let url = "chrome://global/locale/security/caps.properties";
-
-  let strings = StringBundle(url);
-
-  assert.equal(strings.url, url,
-                   "'url' property contains correct URL of string bundle");
-
-  let appLocale = Cc["@mozilla.org/intl/nslocaleservice;1"].
-                  getService(Ci.nsILocaleService).
-                  getApplicationLocale();
-
-  let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"].
-                     getService(Ci.nsIStringBundleService).
-                     createBundle(url, appLocale);
-
-  let (name = "CheckMessage") {
-    assert.equal(strings.get(name), stringBundle.GetStringFromName(name),
-                 "getting a string returns the string");
-  }
-
-  let (name = "CreateWrapperDenied", args = ["foo"]) {
-    assert.equal(strings.get(name, args),
-                 stringBundle.formatStringFromName(name, args, args.length),
-                 "getting a formatted string returns the formatted string");
-  }
-
-  assert.throws(function () strings.get("nonexistentString"),
-                RegExp("String 'nonexistentString' could not be retrieved from " +
-                       "the bundle due to an unknown error \\(it doesn't exist\\?\\)\\."),
-                "retrieving a nonexistent string throws an exception");
-
-  let a = [], b = [];
-  let enumerator = stringBundle.getSimpleEnumeration();
-  while (enumerator.hasMoreElements()) {
-    let elem = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
-    a.push([elem.key, elem.value]);
-  }
-
-  for (let key in strings) {
-    b.push([ key, strings.get(key) ]);
-  }
-
-  // Sort the arrays, because we don't assume enumeration has a set order.
-  // Sort compares [key, val] as string "key,val", which sorts the way we want
-  // it to, so there is no need to provide a custom compare function.
-  a.sort();
-  b.sort();
-
-  assert.equal(a.length, b.length,
-               "the iterator returns the correct number of items");
-
-  for (let i = 0; i < a.length; i++) {
-    assert.equal(a[i][0], b[i][0], "the iterated string's name is correct");
-    assert.equal(a[i][1], b[i][1],
-                     "the iterated string's value is correct");
-  }
-};
-
-require("sdk/test").run(exports);
--- a/addon-sdk/source/test/test-content-symbiont.js
+++ b/addon-sdk/source/test/test-content-symbiont.js
@@ -1,15 +1,15 @@
 /* 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";
 
 const { Cc, Ci } = require('chrome');
-const { Symbiont } = require('sdk/content/symbiont');
+const { Symbiont } = require('sdk/deprecated/symbiont');
 const self = require('sdk/self');
 const fixtures = require("./fixtures");
 const { close } = require('sdk/window/helpers');
 const app = require("sdk/system/xul-app");
 
 function makeWindow() {
   let content =
     '<?xml version="1.0"?>' +
--- a/addon-sdk/source/test/test-content-worker.js
+++ b/addon-sdk/source/test/test-content-worker.js
@@ -7,16 +7,17 @@
 // Skipping due to window creation being unsupported in Fennec
 module.metadata = {
   engines: {
     'Firefox': '*'
   }
 };
 
 const { Cc, Ci } = require("chrome");
+const { on } = require("sdk/event/core");
 const { setTimeout } = require("sdk/timers");
 const { LoaderWithHookedConsole } = require("sdk/test/loader");
 const { Worker } = require("sdk/content/worker");
 const { close } = require("sdk/window/helpers");
 const { set: setPref } = require("sdk/preferences/service");
 const { isArray } = require("sdk/lang/type");
 const { URL } = require('sdk/url');
 const fixtures = require("./fixtures");
@@ -111,16 +112,18 @@ exports["test:sample"] = WorkerTest(
         assert.equal(worker.url, window.location.href,
                          "worker.url still works");
         done();
       }
     });
 
     assert.equal(worker.url, window.location.href,
                      "worker.url works");
+    assert.equal(worker.contentURL, window.location.href,
+                     "worker.contentURL works");
     worker.postMessage("hi!");
   }
 );
 
 exports["test:emit"] = WorkerTest(
   DEFAULT_CONTENT_URL,
   function(assert, browser, done) {
 
@@ -221,17 +224,17 @@ exports["test:post-json-values-only"] = 
 
     let worker =  Worker({
         window: browser.contentWindow,
         contentScript: "new " + function WorkerScope() {
           self.on("message", function (message) {
             self.postMessage([ message.fun === undefined,
                                typeof message.w,
                                message.w && "port" in message.w,
-                               message.w.url,
+                               message.w._url,
                                Array.isArray(message.array),
                                JSON.stringify(message.array)]);
           });
         }
       });
 
     // Validate worker.onMessage
     let array = [1, 2, 3];
@@ -242,16 +245,20 @@ exports["test:post-json-values-only"] = 
       assert.equal(message[3], DEFAULT_CONTENT_URL,
                        "jsonable attributes are accessible");
       // See bug 714891, Arrays may be broken over compartements:
       assert.ok(message[4], "Array keeps being an array");
       assert.equal(message[5], JSON.stringify(array),
                        "Array is correctly serialized");
       done();
     });
+    // Add a new url property sa the Class function used by 
+    // Worker doesn't set enumerables to true for non-functions
+    worker._url = DEFAULT_CONTENT_URL;
+
     worker.postMessage({ fun: function () {}, w: worker, array: array });
   }
 );
 
 exports["test:emit-json-values-only"] = WorkerTest(
   DEFAULT_CONTENT_URL,
   function(assert, browser, done) {
 
@@ -259,17 +266,17 @@ exports["test:emit-json-values-only"] = 
         window: browser.contentWindow,
         contentScript: "new " + function WorkerScope() {
           // Validate self.on and self.emit
           self.port.on("addon-to-content", function (fun, w, obj, array) {
             self.port.emit("content-to-addon", [
                             fun === null,
                             typeof w,
                             "port" in w,
-                            w.url,
+                            w._url,
                             "fun" in obj,
                             Object.keys(obj.dom).length,
                             Array.isArray(array),
                             JSON.stringify(array)
                           ]);
           });
         }
       });
@@ -290,16 +297,19 @@ exports["test:emit-json-values-only"] = 
                        "Array is correctly serialized");
       done();
     });
 
     let obj = {
       fun: function () {},
       dom: browser.contentWindow.document.createElement("div")
     };
+    // Add a new url property sa the Class function used by 
+    // Worker doesn't set enumerables to true for non-functions
+    worker._url = DEFAULT_CONTENT_URL;
     worker.port.emit("addon-to-content", function () {}, worker, obj, array);
   }
 );
 
 exports["test:content is wrapped"] = WorkerTest(
   "data:text/html;charset=utf-8,<script>var documentValue=true;</script>",
   function(assert, browser, done) {
 
@@ -824,9 +834,42 @@ exports['test:conentScriptFile as URL in
         assert.equal(msg, "msg from contentScriptFile", 
             "received a wrong message from contentScriptFile");
         done();
       }
     });
   }
 );
 
+exports.testWorkerEvents = WorkerTest(DEFAULT_CONTENT_URL, function (assert, browser, done) {
+  let window = browser.contentWindow;
+  let events = [];
+  let worker = Worker({
+    window: window,
+    contentScript: 'new ' + function WorkerScope() {
+      self.postMessage('start');
+    },
+    onAttach: win => {
+      events.push('attach');
+      assert.pass('attach event called when attached');
+      assert.equal(window, win, 'attach event passes in attached window');
+    },
+    onError: err => {
+      assert.equal(err.message, 'Custom',
+        'Error passed into error event');
+      worker.detach();
+    },
+    onMessage: msg => {
+      assert.pass('`onMessage` handles postMessage')
+      throw new Error('Custom');
+    },
+    onDetach: _ => {
+      assert.pass('`onDetach` called when worker detached');
+      done();
+    }
+  });
+  // `attach` event is called synchronously during instantiation,
+  // so we can't listen to that, TODO FIX?
+  //  worker.on('attach', obj => console.log('attach', obj));
+});
+
+
 require("test").run(exports);
--- a/addon-sdk/source/test/test-event-core.js
+++ b/addon-sdk/source/test/test-event-core.js
@@ -1,15 +1,15 @@
 /* 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';
 
-const { on, once, off, emit, count, amass } = require('sdk/event/core');
+const { on, once, off, emit, count } = require('sdk/event/core');
 const { LoaderWithHookedConsole } = require("sdk/test/loader");
 
 exports['test add a listener'] = function(assert) {
   let events = [ { name: 'event#1' }, 'event#2' ];
   let target = { name: 'target' };
 
   on(target, 'message', function(message) {
     assert.equal(this, target, 'this is a target object');
--- a/addon-sdk/source/test/test-event-utils.js
+++ b/addon-sdk/source/test/test-event-utils.js
@@ -1,16 +1,16 @@
 /* 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';
 
 const { on, emit } = require("sdk/event/core");
-const { filter, map, merge, expand, pipe } = require("sdk/event/utils");
+const { filter, map, merge, expand, pipe, stripListeners } = require("sdk/event/utils");
 const $ = require("./event/helpers");
 
 function isEven(x) !(x % 2)
 function inc(x) x + 1
 
 exports["test filter events"] = function(assert) {
   let input = {};
   let evens = filter(input, isEven);
@@ -163,55 +163,55 @@ exports["test expand"] = function(assert
 
   assert.deepEqual(actual, ["a1", "b1", "a2", "c1", "c2", "b2", "a3"],
                    "all inputs data merged into one");
 };
 
 exports["test pipe"] = function (assert, done) {
   let src = {};
   let dest = {};
-  
+
   let aneventCount = 0, multiargsCount = 0;
   let wildcardCount = {};
 
   on(dest, 'an-event', arg => {
     assert.equal(arg, 'my-arg', 'piped argument to event');
     ++aneventCount;
     check();
   });
   on(dest, 'multiargs', (...data) => {
     assert.equal(data[0], 'a', 'multiple arguments passed via pipe');
     assert.equal(data[1], 'b', 'multiple arguments passed via pipe');
     assert.equal(data[2], 'c', 'multiple arguments passed via pipe');
     ++multiargsCount;
     check();
   });
-  
+
   on(dest, '*', (name, ...data) => {
     wildcardCount[name] = (wildcardCount[name] || 0) + 1;
     if (name === 'multiargs') {
       assert.equal(data[0], 'a', 'multiple arguments passed via pipe, wildcard');
       assert.equal(data[1], 'b', 'multiple arguments passed via pipe, wildcard');
       assert.equal(data[2], 'c', 'multiple arguments passed via pipe, wildcard');
     }
     if (name === 'an-event')
       assert.equal(data[0], 'my-arg', 'argument passed via pipe, wildcard');
     check();
   });
 
   pipe(src, dest);
 
   for (let i = 0; i < 3; i++)
     emit(src, 'an-event', 'my-arg');
-  
+
   emit(src, 'multiargs', 'a', 'b', 'c');
 
   function check () {
     if (aneventCount === 3 && multiargsCount === 1 &&
-        wildcardCount['an-event'] === 3 && 
+        wildcardCount['an-event'] === 3 &&
         wildcardCount['multiargs'] === 1)
       done();
   }
 };
 
 exports["test pipe multiple targets"] = function (assert) {
   let src1 = {};
   let src2 = {};
@@ -232,27 +232,51 @@ exports["test pipe multiple targets"] = 
   on(middle, '*', () => middleFired++);
   on(dest, '*', () => destFired++);
 
   emit(src1, 'ev');
   assert.equal(src1Fired, 1, 'event triggers in source in pipe chain');
   assert.equal(middleFired, 1, 'event passes through the middle of pipe chain');
   assert.equal(destFired, 1, 'event propagates to end of pipe chain');
   assert.equal(src2Fired, 0, 'event does not fire on alternative chain routes');
-  
+
   emit(src2, 'ev');
   assert.equal(src2Fired, 1, 'event triggers in source in pipe chain');
   assert.equal(middleFired, 2,
     'event passes through the middle of pipe chain from different src');
   assert.equal(destFired, 2,
     'event propagates to end of pipe chain from different src');
   assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes');
-  
+
   emit(middle, 'ev');
   assert.equal(middleFired, 3,
     'event triggers in source of pipe chain');
   assert.equal(destFired, 3,
     'event propagates to end of pipe chain from middle src');
   assert.equal(src1Fired, 1, 'event does not fire on alternative chain routes');
   assert.equal(src2Fired, 1, 'event does not fire on alternative chain routes');
 };
 
+exports['test stripListeners'] = function (assert) {
+  var options = {
+    onAnEvent: noop1,
+    onMessage: noop2,
+    verb: noop1,
+    value: 100
+  };
+
+  var stripped = stripListeners(options);
+  assert.ok(stripped !== options, 'stripListeners should return a new object');
+  assert.equal(options.onAnEvent, noop1, 'stripListeners does not affect original');
+  assert.equal(options.onMessage, noop2, 'stripListeners does not affect original');
+  assert.equal(options.verb, noop1, 'stripListeners does not affect original');
+  assert.equal(options.value, 100, 'stripListeners does not affect original');
+
+  assert.ok(!stripped.onAnEvent, 'stripListeners removes `on*` values');
+  assert.ok(!stripped.onMessage, 'stripListeners removes `on*` values');
+  assert.equal(stripped.verb, noop1, 'stripListeners leaves not `on*` values');
+  assert.equal(stripped.value, 100, 'stripListeners leaves not `on*` values');
+
+  function noop1 () {}
+  function noop2 () {}
+};
+
 require('test').run(exports);
--- a/addon-sdk/source/test/test-loader.js
+++ b/addon-sdk/source/test/test-loader.js
@@ -96,16 +96,23 @@ exports['test syntax errors'] = function
     assert.equal(stack.pop().fileName, module.uri,
                  "previous to it is a test module");
 
   } finally {
     unload(loader);
   }
 }
 
+exports['test sandboxes are not added if error'] = function (assert) {
+  let uri = root + '/fixtures/loader/missing-twice/';
+  let loader = Loader({ paths: { '': uri } });
+  let program = main(loader, 'main');
+  assert.ok(!(uri + 'not-found.js' in loader.sandboxes), 'not-found.js not in loader.sandboxes');
+}
+
 exports['test missing module'] = function(assert) {
   let uri = root + '/fixtures/loader/missing/'
   let loader = Loader({ paths: { '': uri } });
 
   try {
     let program = main(loader, 'main')
   } catch (error) {
     assert.equal(error.message, "Module `not-found` is not found at " +
@@ -123,16 +130,36 @@ exports['test missing module'] = functio
 
     assert.equal(stack.pop().fileName, module.uri,
                  "previous in the stack is test module");
   } finally {
     unload(loader);
   }
 }
 
+exports["test invalid module not cached and throws everytime"] = function(assert) {
+  let uri = root + "/fixtures/loader/missing-twice/";
+  let loader = Loader({ paths: { "": uri } });
+
+  let { firstError, secondError, invalidJSON1, invalidJSON2 } = main(loader, "main");
+  assert.equal(firstError.message, "Module `not-found` is not found at " +
+    uri + "not-found.js", "throws on first invalid require");
+  assert.equal(firstError.lineNumber, 8, "first error is on line 7");
+  assert.equal(secondError.message, "Module `not-found` is not found at " +
+    uri + "not-found.js", "throws on second invalid require");
+  assert.equal(secondError.lineNumber, 14, "second error is on line 14");
+
+  assert.equal(invalidJSON1.message,
+    "JSON.parse: unexpected character at line 1 column 1 of the JSON data",
+    "throws on invalid JSON");
+  assert.equal(invalidJSON2.message,
+    "JSON.parse: unexpected character at line 1 column 1 of the JSON data",
+    "throws on invalid JSON second time");
+};
+
 exports['test exceptions in modules'] = function(assert) {
   let uri = root + '/fixtures/loader/exceptions/'
 
   let loader = Loader({ paths: { '': uri } });
 
   try {
     let program = main(loader, 'main')
   } catch (error) {
--- a/addon-sdk/source/test/test-memory.js
+++ b/addon-sdk/source/test/test-memory.js
@@ -1,21 +1,22 @@
 /* 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";
 
-var memory = require("sdk/deprecated/memory");
+const memory = require("sdk/deprecated/memory");
+const { gc } = require("sdk/test/memory");
 
 exports.testMemory = function(assert) {
-  assert.pass("Skipping this test until Gecko memory debugging issues " +
-            "are resolved (see bug 592774).");
-  return;
-
   var obj = {};
   memory.track(obj, "testMemory.testObj");
+
   var objs = memory.getObjects("testMemory.testObj");
   assert.equal(objs[0].weakref.get(), obj);
   obj = null;
-  memory.gc();
-  assert.equal(objs[0].weakref.get(), null);
+
+  gc().then(function() {
+    assert.equal(objs[0].weakref.get(), null);
+  });
 };
 
 require('sdk/test').run(exports);
deleted file mode 100644
--- a/addon-sdk/source/test/test-observer-service.js
+++ /dev/null
@@ -1,79 +0,0 @@
-/* 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/. */
-
-const observers = require("sdk/deprecated/observer-service");
-const { Cc, Ci } = require("chrome");
-const { LoaderWithHookedConsole2 } = require("sdk/test/loader");
-
-exports.testUnloadAndErrorLogging = function(assert) {
-  let { loader, messages } = LoaderWithHookedConsole2(module);
-  var sbobsvc = loader.require("sdk/deprecated/observer-service");
-
-  var timesCalled = 0;
-  var cb = function(subject, data) {
-    timesCalled++;
-  };
-  var badCb = function(subject, data) {
-    throw new Error("foo");
-  };
-  sbobsvc.add("blarg", cb);
-  observers.notify("blarg", "yo yo");
-  assert.equal(timesCalled, 1);
-  sbobsvc.add("narg", badCb);
-  observers.notify("narg", "yo yo");
-
-  assert.equal(messages[0], "console.error: " + require("sdk/self").name + ": \n");
-  var lines = messages[1].split("\n");
-  assert.equal(lines[0], "  Message: Error: foo");
-  assert.equal(lines[1], "  Stack:");
-  // Keep in mind to update "18" to the line of "throw new Error("foo")"
-  assert.ok(lines[2].indexOf(module.uri + ":18") != -1);
-
-  loader.unload();
-  observers.notify("blarg", "yo yo");
-  assert.equal(timesCalled, 1);
-};
-
-exports.testObserverService = function(assert) {
-  var ios = Cc['@mozilla.org/network/io-service;1']
-            .getService(Ci.nsIIOService);
-  var service = Cc["@mozilla.org/observer-service;1"].
-                getService(Ci.nsIObserverService);
-  var uri = ios.newURI("http://www.foo.com", null, null);
-  var timesCalled = 0;
-  var lastSubject = null;
-  var lastData = null;
-
-  var cb = function(subject, data) {
-    timesCalled++;
-    lastSubject = subject;
-    lastData = data;
-  };
-
-  observers.add("blarg", cb);
-  service.notifyObservers(uri, "blarg", "some data");
-  assert.equal(timesCalled, 1,
-                   "observer-service.add() should call callback");
-  assert.equal(lastSubject, uri,
-                   "observer-service.add() should pass subject");
-  assert.equal(lastData, "some data",
-                   "observer-service.add() should pass data");
-
-  function customSubject() {}
-  function customData() {}
-  observers.notify("blarg", customSubject, customData);
-  assert.equal(timesCalled, 2,
-                   "observer-service.notify() should work");
-  assert.equal(lastSubject, customSubject,
-                   "observer-service.notify() should pass+wrap subject");
-  assert.equal(lastData, customData,
-                   "observer-service.notify() should pass data");
-
-  observers.remove("blarg", cb);
-  service.notifyObservers(null, "blarg", "some data");
-  assert.equal(timesCalled, 2,
-                   "observer-service.remove() should work");
-};
-
-require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-simple-prefs.js
+++ b/addon-sdk/source/test/test-simple-prefs.js
@@ -1,16 +1,16 @@
 /* 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";
 
 const { Loader } = require("sdk/test/loader");
 const { setTimeout } = require("sdk/timers");
-const { notify } = require("sdk/deprecated/observer-service");
+const { emit } = require("sdk/system/events");
 const { id } = require("sdk/self");
 const simplePrefs = require("sdk/simple-prefs");
 const { prefs: sp } = simplePrefs;
 
 const specialChars = "!@#$%^&*()_-=+[]{}~`\'\"<>,./?;:";
 
 exports.testIterations = function(assert) {
   sp["test"] = true;
@@ -127,17 +127,17 @@ exports.testPrefListener = function(asse
 
 exports.testBtnListener = function(assert, done) {
   let name = "test-btn-listen";
   simplePrefs.on(name, function listener() {
     simplePrefs.removeListener(name, listener);
     assert.pass("Button press event was heard");
     done();
   });
-  notify((id + "-cmdPressed"), "", name);
+  emit((id + "-cmdPressed"), { subject: "", data: name });
 };
 
 exports.testPrefRemoveListener = function(assert, done) {
   let counter = 0;
 
   let listener = function() {
     assert.pass("The prefs listener was not removed yet");
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-test-memory.js
@@ -0,0 +1,23 @@
+/* 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';
+
+const { Cc, Ci, Cu, components } = require('chrome');
+const { gc } = require('sdk/test/memory');
+
+exports.testGC = function(assert, done) {
+  let weakref;
+  let (tempObj = {}) {
+    weakref = Cu.getWeakReference(tempObj);
+    assert.equal(weakref.get(), tempObj, 'the weakref returned the tempObj');
+  }
+
+  gc().then(function(arg) {
+    assert.equal(arg, undefined, 'there is no argument');
+    assert.pass('gc() returns a promise which eventually resolves');
+    assert.equal(weakref.get(), undefined, 'the weakref returned undefined');
+  }).then(done).then(null, assert.fail);
+};
+
+require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-traceback.js
+++ b/addon-sdk/source/test/test-traceback.js
@@ -1,46 +1,47 @@
 /* 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";
 
 var traceback = require("sdk/console/traceback");
 var {Cc,Ci,Cr,Cu} = require("chrome");
+const { on, off } = require("sdk/system/events");
 
 function throwNsIException() {
   var ios = Cc['@mozilla.org/network/io-service;1']
             .getService(Ci.nsIIOService);
   ios.newURI("i'm a malformed URI", null, null);
 }
 
 function throwError() {
   throw new Error("foob");
 }
 
 exports.testFormatDoesNotFetchRemoteFiles = function(assert) {
-  var observers = require("sdk/deprecated/observer-service");
   ["http", "https"].forEach(
     function(scheme) {
       var httpRequests = 0;
       function onHttp() {
         httpRequests++;
       }
 
-      observers.add("http-on-modify-request", onHttp);
+      on("http-on-modify-request", onHttp);
 
       try {
         var tb = [{filename: scheme + "://www.mozilla.org/",
                    lineNumber: 1,
                    name: "blah"}];
         traceback.format(tb);
       } catch (e) {
         assert.fail(e);
       }
 
-      observers.remove("http-on-modify-request", onHttp);
+      off("http-on-modify-request", onHttp);
 
       assert.equal(httpRequests, 0,
                        "traceback.format() does not make " +
                        scheme + " request");
     });
 };
 
 exports.testFromExceptionWithString = function(assert) {
--- a/addon-sdk/source/test/test-window-utils2.js
+++ b/addon-sdk/source/test/test-window-utils2.js
@@ -6,17 +6,17 @@
 // Opening new windows in Fennec causes issues
 module.metadata = {
   engines: {
     'Firefox': '*'
   }
 };
 
 const { Ci } = require('chrome');
-const { open, backgroundify, windows, isBrowser,
+const { open, windows, isBrowser,
         getXULWindow, getBaseWindow, getToplevelWindow, getMostRecentWindow,
         getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { close } = require('sdk/window/helpers');
 const windowUtils = require('sdk/deprecated/window-utils');
 
 exports['test get nsIBaseWindow from nsIDomWindow'] = function(assert) {
   let active = windowUtils.activeBrowserWindow;
 
@@ -73,50 +73,34 @@ exports['test new top window with variou
   assert.throws(function () {
     open('foo');
   }, msg);
   assert.throws(function () {
     open('http://foo');
   }, msg);
   assert.throws(function () {
     open('https://foo');
-  }, msg); 
+  }, msg);
   assert.throws(function () {
     open('ftp://foo');
   }, msg);
   assert.throws(function () {
     open('//foo');
   }, msg);
 
   let chromeWindow = open('chrome://foo/content/');
   assert.ok(~windows().indexOf(chromeWindow), 'chrome URI works');
-  
+
   let resourceWindow = open('resource://foo');
   assert.ok(~windows().indexOf(resourceWindow), 'resource URI works');
 
   // Wait for the window unload before ending test
   close(chromeWindow).then(close.bind(null, resourceWindow)).then(done);
 };
 
-exports.testBackgroundify = function(assert, done) {
-  let window = open('data:text/html;charset=utf-8,backgroundy');
-  assert.ok(~windows().indexOf(window),
-            'window is in the list of windows');
-  let backgroundy = backgroundify(window);
-  assert.equal(backgroundy, window, 'backgroundify returs give window back');
-  assert.ok(!~windows().indexOf(window),
-            'backgroundifyied window is in the list of windows');
-
-  // Wait for the window unload before ending test
-  // backgroundified windows doesn't dispatch domwindowclosed event
-  // so that we have to manually wait for unload event
-  window.onunload = done;
-  window.close();
-};
-
 exports.testIsBrowser = function(assert) {
   // dummy window, bad type
   assert.equal(isBrowser({ document: { documentElement: { getAttribute: function() {
     return 'navigator:browserx';
   }}}}), false, 'dummy object with correct stucture and bad type does not pass');
 
   assert.ok(isBrowser(getMostRecentBrowserWindow()), 'active browser window is a browser window');
   assert.ok(!isBrowser({}), 'non window is not a browser window');
--- a/addon-sdk/source/test/test-windows-common.js
+++ b/addon-sdk/source/test/test-windows-common.js
@@ -1,20 +1,16 @@
 /* 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';
 
 const { Loader } = require('sdk/test/loader');
 const { browserWindows } = require('sdk/windows');
-const { viewFor } = require('sdk/view/core');
 const { Ci } = require("chrome");
-const { isBrowser, getWindowTitle } = require("sdk/window/utils");
-const { defer } = require("sdk/lang/functional");
-
 
 // TEST: browserWindows Iterator
 exports.testBrowserWindowsIterator = function(assert) {
   let activeWindowCount = 0;
   let windows = [];
   let i = 0;
   for each (let window in browserWindows) {
     if (window === browserWindows.activeWindow)
@@ -56,31 +52,9 @@ exports.testWindowActivateMethod_simple 
   window.activate();
 
   assert.equal(browserWindows.activeWindow, window,
                'Active window is active after window.activate() call');
   assert.equal(window.tabs.activeTab, tab,
                'Active tab is active after window.activate() call');
 };
 
-
-exports["test getView(window)"] = function(assert, done) {
-  browserWindows.once("open", window => {
-    const view = viewFor(window);
-
-    assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window");
-    assert.ok(isBrowser(view), "view is a browser window");
-    assert.equal(getWindowTitle(view), window.title,
-                 "window has a right title");
-
-    window.close();
-    // Defer handler cause window is destroyed after event is dispatched.
-    browserWindows.once("close", defer(_ => {
-      assert.equal(viewFor(window), null, "window view is gone");
-      done();
-    }));
-  });
-
-
-  browserWindows.open({ url: "data:text/html,<title>yo</title>" });
-};
-
 require('sdk/test').run(exports);
--- a/addon-sdk/source/test/windows/test-firefox-windows.js
+++ b/addon-sdk/source/test/windows/test-firefox-windows.js
@@ -1,24 +1,26 @@
 /* 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';
 
 const { Cc, Ci } = require('chrome');
 const { setTimeout } = require('sdk/timers');
 const { Loader } = require('sdk/test/loader');
-const { onFocus, getMostRecentWindow, windows } = require('sdk/window/utils');
+const { onFocus, getMostRecentWindow, windows, isBrowser, getWindowTitle } = require('sdk/window/utils');
 const { open, close, focus } = require('sdk/window/helpers');
 const { browserWindows } = require("sdk/windows");
 const tabs = require("sdk/tabs");
 const winUtils = require("sdk/deprecated/window-utils");
 const { WindowTracker } = winUtils;
 const { isPrivate } = require('sdk/private-browsing');
 const { isWindowPBSupported } = require('sdk/private-browsing/utils');
+const { viewFor } = require("sdk/view/core");
+const { defer } = require("sdk/lang/functional");
 
 // TEST: open & close window
 exports.testOpenAndCloseWindow = function(assert, done) {
   assert.equal(browserWindows.length, 1, "Only one window open");
   let title = 'testOpenAndCloseWindow';
 
   browserWindows.open({
     url: "data:text/html;charset=utf-8,<title>" + title + "</title>",
@@ -413,11 +415,31 @@ exports.testWindowIteratorPrivateDefault
     assert.equal(windows(null, { includePrivate: true }).length, 2);
 
     // test that all windows in iterator are not private
     for (let window of browserWindows)
       assert.ok(!isPrivate(window), 'no window in browserWindows is private');
 
     close(window).then(done);
   });
-}
+};
+
+exports["test getView(window)"] = function(assert, done) {
+  browserWindows.once("open", window => {
+    const view = viewFor(window);
+
+    assert.ok(view instanceof Ci.nsIDOMWindow, "view is a window");
+    assert.ok(isBrowser(view), "view is a browser window");
+    assert.equal(getWindowTitle(view), window.title,
+                 "window has a right title");
+
+    window.close();
+    // Defer handler cause window is destroyed after event is dispatched.
+    browserWindows.once("close", defer(_ => {
+      assert.equal(viewFor(window), null, "window view is gone");
+      done();
+    }));
+  });
+
+  browserWindows.open({ url: "data:text/html,<title>yo</title>" });
+};
 
 require('sdk/test').run(exports);