addon-sdk/source/lib/sdk/content/content-worker.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/addon-sdk/source/lib/sdk/content/content-worker.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,342 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +const ContentWorker = Object.freeze({
     1.9 +  // TODO: Bug 727854 Use same implementation than common JS modules,
    1.10 +  // i.e. EventEmitter module
    1.11 +
    1.12 +  /**
    1.13 +   * Create an EventEmitter instance.
    1.14 +   */
    1.15 +  createEventEmitter: function createEventEmitter(emit) {
    1.16 +    let listeners = Object.create(null);
    1.17 +    let eventEmitter = Object.freeze({
    1.18 +      emit: emit,
    1.19 +      on: function on(name, callback) {
    1.20 +        if (typeof callback !== "function")
    1.21 +          return this;
    1.22 +        if (!(name in listeners))
    1.23 +          listeners[name] = [];
    1.24 +        listeners[name].push(callback);
    1.25 +        return this;
    1.26 +      },
    1.27 +      once: function once(name, callback) {
    1.28 +        eventEmitter.on(name, function onceCallback() {
    1.29 +          eventEmitter.removeListener(name, onceCallback);
    1.30 +          callback.apply(callback, arguments);
    1.31 +        });
    1.32 +      },
    1.33 +      removeListener: function removeListener(name, callback) {
    1.34 +        if (!(name in listeners))
    1.35 +          return;
    1.36 +        let index = listeners[name].indexOf(callback);
    1.37 +        if (index == -1)
    1.38 +          return;
    1.39 +        listeners[name].splice(index, 1);
    1.40 +      }
    1.41 +    });
    1.42 +    function onEvent(name) {
    1.43 +      if (!(name in listeners))
    1.44 +        return [];
    1.45 +      let args = Array.slice(arguments, 1);
    1.46 +      let results = [];
    1.47 +      for each (let callback in listeners[name]) {
    1.48 +        results.push(callback.apply(null, args));
    1.49 +      }
    1.50 +      return results;
    1.51 +    }
    1.52 +    function hasListenerFor(name) {
    1.53 +      if (!(name in listeners))
    1.54 +        return false;
    1.55 +      return listeners[name].length > 0;
    1.56 +    }
    1.57 +    return {
    1.58 +      eventEmitter: eventEmitter,
    1.59 +      emit: onEvent,
    1.60 +      hasListenerFor: hasListenerFor
    1.61 +    };
    1.62 +  },
    1.63 +
    1.64 +  /**
    1.65 +   * Create an EventEmitter instance to communicate with chrome module
    1.66 +   * by passing only strings between compartments.
    1.67 +   * This function expects `emitToChrome` function, that allows to send
    1.68 +   * events to the chrome module. It returns the EventEmitter as `pipe`
    1.69 +   * attribute, and, `onChromeEvent` a function that allows chrome module
    1.70 +   * to send event into the EventEmitter.
    1.71 +   *
    1.72 +   *                  pipe.emit --> emitToChrome
    1.73 +   *              onChromeEvent --> callback registered through pipe.on
    1.74 +   */
    1.75 +  createPipe: function createPipe(emitToChrome) {
    1.76 +    function onEvent(type, ...args) {
    1.77 +      // JSON.stringify is buggy with cross-sandbox values,
    1.78 +      // it may return "{}" on functions. Use a replacer to match them correctly.
    1.79 +      let replacer = (k, v) =>
    1.80 +        typeof(v) === "function"
    1.81 +          ? (type === "console" ? Function.toString.call(v) : void(0))
    1.82 +          : v;
    1.83 +
    1.84 +      let str = JSON.stringify([type, ...args], replacer);
    1.85 +      emitToChrome(str);
    1.86 +    }
    1.87 +
    1.88 +    let { eventEmitter, emit, hasListenerFor } =
    1.89 +      ContentWorker.createEventEmitter(onEvent);
    1.90 +
    1.91 +    return {
    1.92 +      pipe: eventEmitter,
    1.93 +      onChromeEvent: function onChromeEvent(array) {
    1.94 +        // We either receive a stringified array, or a real array.
    1.95 +        // We still allow to pass an array of objects, in WorkerSandbox.emitSync
    1.96 +        // in order to allow sending DOM node reference between content script
    1.97 +        // and modules (only used for context-menu API)
    1.98 +        let args = typeof array == "string" ? JSON.parse(array) : array;
    1.99 +        return emit.apply(null, args);
   1.100 +      },
   1.101 +      hasListenerFor: hasListenerFor
   1.102 +    };
   1.103 +  },
   1.104 +
   1.105 +  injectConsole: function injectConsole(exports, pipe) {
   1.106 +    exports.console = Object.freeze({
   1.107 +      log: pipe.emit.bind(null, "console", "log"),
   1.108 +      info: pipe.emit.bind(null, "console", "info"),
   1.109 +      warn: pipe.emit.bind(null, "console", "warn"),
   1.110 +      error: pipe.emit.bind(null, "console", "error"),
   1.111 +      debug: pipe.emit.bind(null, "console", "debug"),
   1.112 +      exception: pipe.emit.bind(null, "console", "exception"),
   1.113 +      trace: pipe.emit.bind(null, "console", "trace"),
   1.114 +      time: pipe.emit.bind(null, "console", "time"),
   1.115 +      timeEnd: pipe.emit.bind(null, "console", "timeEnd")
   1.116 +    });
   1.117 +  },
   1.118 +
   1.119 +  injectTimers: function injectTimers(exports, chromeAPI, pipe, console) {
   1.120 +    // wrapped functions from `'timer'` module.
   1.121 +    // Wrapper adds `try catch` blocks to the callbacks in order to
   1.122 +    // emit `error` event on a symbiont if exception is thrown in
   1.123 +    // the Worker global scope.
   1.124 +    // @see http://www.w3.org/TR/workers/#workerutils
   1.125 +
   1.126 +    // List of all living timeouts/intervals
   1.127 +    let _timers = Object.create(null);
   1.128 +
   1.129 +    // Keep a reference to original timeout functions
   1.130 +    let {
   1.131 +      setTimeout: chromeSetTimeout,
   1.132 +      setInterval: chromeSetInterval,
   1.133 +      clearTimeout: chromeClearTimeout,
   1.134 +      clearInterval: chromeClearInterval
   1.135 +    } = chromeAPI.timers;
   1.136 +
   1.137 +    function registerTimer(timer) {
   1.138 +      let registerMethod = null;
   1.139 +      if (timer.kind == "timeout")
   1.140 +        registerMethod = chromeSetTimeout;
   1.141 +      else if (timer.kind == "interval")
   1.142 +        registerMethod = chromeSetInterval;
   1.143 +      else
   1.144 +        throw new Error("Unknown timer kind: " + timer.kind);
   1.145 +
   1.146 +      if (typeof timer.fun == 'string') {
   1.147 +        let code = timer.fun;
   1.148 +        timer.fun = () => chromeAPI.sandbox.evaluate(exports, code);
   1.149 +      } else if (typeof timer.fun != 'function') {
   1.150 +        throw new Error('Unsupported callback type' + typeof timer.fun);
   1.151 +      }
   1.152 +
   1.153 +      let id = registerMethod(onFire, timer.delay);
   1.154 +      function onFire() {
   1.155 +        try {
   1.156 +          if (timer.kind == "timeout")
   1.157 +            delete _timers[id];
   1.158 +          timer.fun.apply(null, timer.args);
   1.159 +        } catch(e) {
   1.160 +          console.exception(e);
   1.161 +          let wrapper = {
   1.162 +            instanceOfError: instanceOf(e, Error),
   1.163 +            value: e,
   1.164 +          };
   1.165 +          if (wrapper.instanceOfError) {
   1.166 +            wrapper.value = {
   1.167 +              message: e.message,
   1.168 +              fileName: e.fileName,
   1.169 +              lineNumber: e.lineNumber,
   1.170 +              stack: e.stack,
   1.171 +              name: e.name,
   1.172 +            };
   1.173 +          }
   1.174 +          pipe.emit('error', wrapper);
   1.175 +        }
   1.176 +      }
   1.177 +      _timers[id] = timer;
   1.178 +      return id;
   1.179 +    }
   1.180 +
   1.181 +    // copied from sdk/lang/type.js since modules are not available here
   1.182 +    function instanceOf(value, Type) {
   1.183 +      var isConstructorNameSame;
   1.184 +      var isConstructorSourceSame;
   1.185 +
   1.186 +      // If `instanceof` returned `true` we know result right away.
   1.187 +      var isInstanceOf = value instanceof Type;
   1.188 +
   1.189 +      // If `instanceof` returned `false` we do ducktype check since `Type` may be
   1.190 +      // from a different sandbox. If a constructor of the `value` or a constructor
   1.191 +      // of the value's prototype has same name and source we assume that it's an
   1.192 +      // instance of the Type.
   1.193 +      if (!isInstanceOf && value) {
   1.194 +        isConstructorNameSame = value.constructor.name === Type.name;
   1.195 +        isConstructorSourceSame = String(value.constructor) == String(Type);
   1.196 +        isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) ||
   1.197 +                        instanceOf(Object.getPrototypeOf(value), Type);
   1.198 +      }
   1.199 +      return isInstanceOf;
   1.200 +    }
   1.201 +
   1.202 +    function unregisterTimer(id) {
   1.203 +      if (!(id in _timers))
   1.204 +        return;
   1.205 +      let { kind } = _timers[id];
   1.206 +      delete _timers[id];
   1.207 +      if (kind == "timeout")
   1.208 +        chromeClearTimeout(id);
   1.209 +      else if (kind == "interval")
   1.210 +        chromeClearInterval(id);
   1.211 +      else
   1.212 +        throw new Error("Unknown timer kind: " + kind);
   1.213 +    }
   1.214 +
   1.215 +    function disableAllTimers() {
   1.216 +      Object.keys(_timers).forEach(unregisterTimer);
   1.217 +    }
   1.218 +
   1.219 +    exports.setTimeout = function ContentScriptSetTimeout(callback, delay) {
   1.220 +      return registerTimer({
   1.221 +        kind: "timeout",
   1.222 +        fun: callback,
   1.223 +        delay: delay,
   1.224 +        args: Array.slice(arguments, 2)
   1.225 +      });
   1.226 +    };
   1.227 +    exports.clearTimeout = function ContentScriptClearTimeout(id) {
   1.228 +      unregisterTimer(id);
   1.229 +    };
   1.230 +
   1.231 +    exports.setInterval = function ContentScriptSetInterval(callback, delay) {
   1.232 +      return registerTimer({
   1.233 +        kind: "interval",
   1.234 +        fun: callback,
   1.235 +        delay: delay,
   1.236 +        args: Array.slice(arguments, 2)
   1.237 +      });
   1.238 +    };
   1.239 +    exports.clearInterval = function ContentScriptClearInterval(id) {
   1.240 +      unregisterTimer(id);
   1.241 +    };
   1.242 +
   1.243 +    // On page-hide, save a list of all existing timers before disabling them,
   1.244 +    // in order to be able to restore them on page-show.
   1.245 +    // These events are fired when the page goes in/out of bfcache.
   1.246 +    // https://developer.mozilla.org/En/Working_with_BFCache
   1.247 +    let frozenTimers = [];
   1.248 +    pipe.on("pageshow", function onPageShow() {
   1.249 +      frozenTimers.forEach(registerTimer);
   1.250 +    });
   1.251 +    pipe.on("pagehide", function onPageHide() {
   1.252 +      frozenTimers = [];
   1.253 +      for (let id in _timers)
   1.254 +        frozenTimers.push(_timers[id]);
   1.255 +      disableAllTimers();
   1.256 +      // Some other pagehide listeners may register some timers that won't be
   1.257 +      // frozen as this particular pagehide listener is called first.
   1.258 +      // So freeze these timers on next cycle.
   1.259 +      chromeSetTimeout(function () {
   1.260 +        for (let id in _timers)
   1.261 +          frozenTimers.push(_timers[id]);
   1.262 +        disableAllTimers();
   1.263 +      }, 0);
   1.264 +    });
   1.265 +
   1.266 +    // Unregister all timers when the page is destroyed
   1.267 +    // (i.e. when it is removed from bfcache)
   1.268 +    pipe.on("detach", function clearTimeouts() {
   1.269 +      disableAllTimers();
   1.270 +      _timers = {};
   1.271 +      frozenTimers = [];
   1.272 +    });
   1.273 +  },
   1.274 +
   1.275 +  injectMessageAPI: function injectMessageAPI(exports, pipe, console) {
   1.276 +
   1.277 +    let { eventEmitter: port, emit : portEmit } =
   1.278 +      ContentWorker.createEventEmitter(pipe.emit.bind(null, "event"));
   1.279 +    pipe.on("event", portEmit);
   1.280 +
   1.281 +    let self = {
   1.282 +      port: port,
   1.283 +      postMessage: pipe.emit.bind(null, "message"),
   1.284 +      on: pipe.on.bind(null),
   1.285 +      once: pipe.once.bind(null),
   1.286 +      removeListener: pipe.removeListener.bind(null),
   1.287 +    };
   1.288 +    Object.defineProperty(exports, "self", {
   1.289 +      value: self
   1.290 +    });
   1.291 +
   1.292 +    exports.on = function deprecatedOn() {
   1.293 +      console.error("DEPRECATED: The global `on()` function in content " +
   1.294 +                    "scripts is deprecated in favor of the `self.on()` " +
   1.295 +                    "function, which works the same. Replace calls to `on()` " +
   1.296 +                    "with calls to `self.on()`" +
   1.297 +                    "For more info on `self.on`, see " +
   1.298 +                    "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.");
   1.299 +      return self.on.apply(null, arguments);
   1.300 +    };
   1.301 +
   1.302 +    // Deprecated use of `onMessage` from globals
   1.303 +    let onMessage = null;
   1.304 +    Object.defineProperty(exports, "onMessage", {
   1.305 +      get: function () onMessage,
   1.306 +      set: function (v) {
   1.307 +        if (onMessage)
   1.308 +          self.removeListener("message", onMessage);
   1.309 +        console.error("DEPRECATED: The global `onMessage` function in content" +
   1.310 +                      "scripts is deprecated in favor of the `self.on()` " +
   1.311 +                      "function. Replace `onMessage = function (data){}` " +
   1.312 +                      "definitions with calls to `self.on('message', " +
   1.313 +                      "function (data){})`. " +
   1.314 +                      "For more info on `self.on`, see " +
   1.315 +                      "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.");
   1.316 +        onMessage = v;
   1.317 +        if (typeof onMessage == "function")
   1.318 +          self.on("message", onMessage);
   1.319 +      }
   1.320 +    });
   1.321 +  },
   1.322 +
   1.323 +  injectOptions: function (exports, options) {
   1.324 +    Object.defineProperty( exports.self, "options", { value: JSON.parse( options ) });
   1.325 +  },
   1.326 +
   1.327 +  inject: function (exports, chromeAPI, emitToChrome, options) {
   1.328 +    let { pipe, onChromeEvent, hasListenerFor } =
   1.329 +      ContentWorker.createPipe(emitToChrome);
   1.330 +
   1.331 +    ContentWorker.injectConsole(exports, pipe);
   1.332 +    ContentWorker.injectTimers(exports, chromeAPI, pipe, exports.console);
   1.333 +    ContentWorker.injectMessageAPI(exports, pipe, exports.console);
   1.334 +    if ( options !== undefined ) {
   1.335 +      ContentWorker.injectOptions(exports, options);
   1.336 +    }
   1.337 +
   1.338 +    Object.freeze( exports.self );
   1.339 +
   1.340 +    return {
   1.341 +      emitToContent: onChromeEvent,
   1.342 +      hasListenerFor: hasListenerFor
   1.343 +    };
   1.344 +  }
   1.345 +});

mercurial