diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-09 06:46:43 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-09 06:46:43 -0500 |
commit | ac46df8daea09899ce30dc8fd70986e258c746bf (patch) | |
tree | 2750d3125fc253fd5b0671e4bd268eff1fd97296 /toolkit/jetpack/sdk/deprecated/sync-worker.js | |
parent | 8cecf8d5208f3945b35f879bba3015bb1a11bec6 (diff) | |
download | UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.gz UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.lz UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.xz UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.zip |
Move Add-on SDK source to toolkit/jetpack
Diffstat (limited to 'toolkit/jetpack/sdk/deprecated/sync-worker.js')
-rw-r--r-- | toolkit/jetpack/sdk/deprecated/sync-worker.js | 288 |
1 files changed, 288 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/deprecated/sync-worker.js b/toolkit/jetpack/sdk/deprecated/sync-worker.js new file mode 100644 index 000000000..71cadac36 --- /dev/null +++ b/toolkit/jetpack/sdk/deprecated/sync-worker.js @@ -0,0 +1,288 @@ +/* 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/sync-worker` was previously `content/worker`, that was + * incompatible with e10s. we are in the process of switching to the new + * asynchronous `Worker`, which behaves slightly differently in some edge + * cases, so we are keeping this one around for a short period. + * try to switch to the new one as soon as possible.. + * + */ + +"use strict"; + +module.metadata = { + "stability": "unstable" +}; + +const { Class } = require('../core/heritage'); +const { EventTarget } = require('../event/target'); +const { on, off, emit, setListeners } = require('../event/core'); +const { + attach, detach, destroy +} = require('../content/utils'); +const { method } = require('../lang/functional'); +const { Ci, Cu, Cc } = require('chrome'); +const unload = require('../system/unload'); +const events = require('../system/events'); +const { getInnerId } = require("../window/utils"); +const { WorkerSandbox } = require('../content/sandbox'); +const { isPrivate } = require('../private-browsing/utils'); + +// A weak map of workers to hold private attributes that +// should not be exposed +const workers = new WeakMap(); + +var 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."; + +/** + * Message-passing facility for communication between code running + * in the content and add-on process. + * @see https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/content_worker + */ +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 || {}; + + 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 + * worker. + * + * `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 model = modelFor(this); + let args = ['message'].concat(data); + if (!model.inited) { + model.earlyEvents.push(args); + return; + } + processMessage.apply(null, [this].concat(args)); + }, + + get url () { + let model = modelFor(this); + // model.window will be null after detach + return model.window ? model.window.document.location.href : null; + }, + + get contentURL () { + let model = modelFor(this); + return model.window ? model.window.document.URL : null; + }, + + // Implemented to provide some of the previous features of exposing sandbox + // so that Worker can be extended + getSandbox: function () { + return modelFor(this).contentWorker; + }, + + 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); + + // will set model.contentWorker pointing to the private API: + model.contentWorker = WorkerSandbox(worker, model.window); + + // 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); + + // Mainly enable worker.port.emit to send event to the content worker + model.inited = true; + model.frozen = false; + + // Fire off `attach` event + emit(worker, 'attach', window); + + // Process all events and messages that were fired before the + // worker was initialized. + model.earlyEvents.forEach(args => processMessage.apply(null, [worker].concat(args))); +}); + +/** + * 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, reason) { + let model = modelFor(worker); + + // maybe unloaded before content side is created + if (model.contentWorker) { + model.contentWorker.destroy(reason); + } + + 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; +}); + +isPrivate.define(Worker, ({ tab }) => isPrivate(tab)); + +/** + * Tells content worker to unload itself and + * removes all the references from itself. + */ +destroy.define(Worker, function (worker, reason) { + detach(worker, reason); + modelFor(worker).inited = true; + // Specifying no type or listener removes all listeners + // from target + off(worker); + off(worker.port); +}); + +/** + * 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 (worker, ...args) { + let model = modelFor(worker) || {}; + if (!model.contentWorker) + throw new Error(ERR_DESTROYED); + if (model.frozen) + throw new Error(ERR_FROZEN); + model.contentWorker.emit.apply(null, args); +} + +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)); +} |