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/event | |
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/event')
-rw-r--r-- | toolkit/jetpack/sdk/event/chrome.js | 65 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/event/core.js | 193 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/event/dom.js | 78 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/event/target.js | 74 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/event/utils.js | 328 |
5 files changed, 738 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/event/chrome.js b/toolkit/jetpack/sdk/event/chrome.js new file mode 100644 index 000000000..9044fef99 --- /dev/null +++ b/toolkit/jetpack/sdk/event/chrome.js @@ -0,0 +1,65 @@ +/* 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 { Cc, Ci, Cr, Cu } = require("chrome"); +const { emit, on, off } = require("./core"); +var observerService = Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService); + +const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm"); +const addObserver = ShimWaiver.getProperty(observerService, "addObserver"); +const removeObserver = ShimWaiver.getProperty(observerService, "removeObserver"); + +const { when: unload } = require("../system/unload"); + +// Simple class that can be used to instantiate event channel that +// implements `nsIObserver` interface. It's will is used by `observe` +// function as observer + event target. It basically proxies observer +// notifications as to it's registered listeners. +function ObserverChannel() {} +Object.freeze(Object.defineProperties(ObserverChannel.prototype, { + QueryInterface: { + value: function(iid) { + if (!iid.equals(Ci.nsIObserver) && + !iid.equals(Ci.nsISupportsWeakReference) && + !iid.equals(Ci.nsISupports)) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + } + }, + observe: { + value: function(subject, topic, data) { + emit(this, "data", { + type: topic, + target: subject, + data: data + }); + } + } +})); + +function observe(topic) { + let observerChannel = new ObserverChannel(); + + // Note: `nsIObserverService` will not hold a weak reference to a + // observerChannel (since third argument is `true`). There for if it + // will be GC-ed with all it's event listeners once no other references + // will be held. + addObserver(observerChannel, topic, true); + + // We need to remove any observer added once the add-on is unloaded; + // otherwise we'll get a "dead object" exception. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1001833 + unload(() => removeObserver(observerChannel, topic)); + + return observerChannel; +} + +exports.observe = observe; diff --git a/toolkit/jetpack/sdk/event/core.js b/toolkit/jetpack/sdk/event/core.js new file mode 100644 index 000000000..c16dd2df5 --- /dev/null +++ b/toolkit/jetpack/sdk/event/core.js @@ -0,0 +1,193 @@ +/* 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 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] = []; +}; + +/** + * Registers an event `listener` that is called every time events of + * specified `type` is emitted on the given event `target`. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ +function on(target, type, listener) { + if (typeof(listener) !== 'function') + throw new Error(BAD_LISTENER); + + let listeners = observers(target, type); + if (!~listeners.indexOf(listener)) + listeners.push(listener); +} +exports.on = on; + + +var onceWeakMap = new WeakMap(); + + +/** + * Registers an event `listener` that is called only the next time an event + * of the specified `type` is emitted on the given event `target`. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of the event. + * @param {Function} listener + * The listener function that processes the event. + */ +function once(target, type, listener) { + let replacement = function observer(...args) { + off(target, type, observer); + onceWeakMap.delete(listener); + listener.apply(target, args); + }; + onceWeakMap.set(listener, replacement); + on(target, type, replacement); +} +exports.once = once; + +/** + * Execute each of the listeners in order with the supplied arguments. + * All the exceptions that are thrown by listeners during the emit + * are caught and can be handled by listeners of 'error' event. Thrown + * exceptions are passed as an argument to an 'error' event listener. + * If no 'error' listener is registered exception will be logged into an + * error console. + * @param {Object} target + * Event target object. + * @param {String} type + * The type of event. + * @params {Object|Number|String|Boolean} args + * Arguments that will be passed to listeners. + */ +function emit (target, type, ...args) { + emitOnObject(target, type, target, ...args); +} +exports.emit = emit; + +/** + * A variant of emit that allows setting the this property for event listeners + */ +function emitOnObject(target, type, thisArg, ...args) { + let all = observers(target, '*').length; + let state = observers(target, type); + let listeners = state.slice(); + let count = listeners.length; + let index = 0; + + // If error event and there are no handlers (explicit or catch-all) + // then print error message to the console. + if (count === 0 && type === 'error' && all === 0) + console.exception(args[0]); + while (index < count) { + try { + let listener = listeners[index]; + // Dispatch only if listener is still registered. + if (~state.indexOf(listener)) + listener.apply(thisArg, args); + } + catch (error) { + // If exception is not thrown by a error listener and error listener is + // registered emit `error` event. Otherwise dump exception to the console. + if (type !== 'error') emit(target, 'error', error); + else console.exception(error); + } + index++; + } + // Also emit on `"*"` so that one could listen for all events. + if (type !== '*') emit(target, '*', type, ...args); +} +exports.emitOnObject = emitOnObject; + +/** + * Removes an event `listener` for the given event `type` on the given event + * `target`. If no `listener` is passed removes all listeners of the given + * `type`. If `type` is not passed removes all the listeners of the given + * event `target`. + * @param {Object} target + * The event target object. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ +function off(target, type, listener) { + let length = arguments.length; + if (length === 3) { + if (onceWeakMap.has(listener)) { + listener = onceWeakMap.get(listener); + onceWeakMap.delete(listener); + } + + let listeners = observers(target, type); + let index = listeners.indexOf(listener); + if (~index) + listeners.splice(index, 1); + } + else if (length === 2) { + observers(target, type).splice(0); + } + else if (length === 1) { + let listeners = event(target); + Object.keys(listeners).forEach(type => delete listeners[type]); + } +} +exports.off = off; + +/** + * Returns a number of event listeners registered for the given event `type` + * on the given event `target`. + */ +function count(target, type) { + return observers(target, type).length; +} +exports.count = count; + +/** + * Registers listeners on the given event `target` from the given `listeners` + * dictionary. Iterates over the listeners and if property name matches name + * pattern `onEventType` and property is a function, then registers it as + * an `eventType` listener on `target`. + * + * @param {Object} target + * 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(); + if (!type) return; + + let listener = listeners[key]; + if (typeof(listener) === 'function') + on(target, type, listener); + }); +} +exports.setListeners = setListeners; diff --git a/toolkit/jetpack/sdk/event/dom.js b/toolkit/jetpack/sdk/event/dom.js new file mode 100644 index 000000000..da99dec7a --- /dev/null +++ b/toolkit/jetpack/sdk/event/dom.js @@ -0,0 +1,78 @@ +/* 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 { Ci } = require("chrome"); + +var { emit } = require("./core"); +var { when: unload } = require("../system/unload"); +var listeners = new WeakMap(); + +const { Cu } = require("chrome"); +const { ShimWaiver } = Cu.import("resource://gre/modules/ShimWaiver.jsm"); +const { ThreadSafeChromeUtils } = Cu.import("resource://gre/modules/Services.jsm", {}); + +var getWindowFrom = x => + x instanceof Ci.nsIDOMWindow ? x : + x instanceof Ci.nsIDOMDocument ? x.defaultView : + x instanceof Ci.nsIDOMNode ? x.ownerDocument.defaultView : + null; + +function removeFromListeners() { + ShimWaiver.getProperty(this, "removeEventListener")("DOMWindowClose", removeFromListeners); + for (let cleaner of listeners.get(this)) + cleaner(); + + listeners.delete(this); +} + +// Simple utility function takes event target, event type and optional +// `options.capture` and returns node style event stream that emits "data" +// events every time event of that type occurs on the given `target`. +function open(target, type, options) { + let output = {}; + let capture = options && options.capture ? true : false; + let listener = (event) => emit(output, "data", event); + + // `open` is currently used only on DOM Window objects, however it was made + // to be used to any kind of `target` that supports `addEventListener`, + // therefore is safer get the `window` from the `target` instead assuming + // that `target` is the `window`. + let window = getWindowFrom(target); + + // If we're not able to get a `window` from `target`, there is something + // wrong. We cannot add listeners that can leak later, or results in + // "dead object" exception. + // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1001833 + if (!window) + throw new Error("Unable to obtain the owner window from the target given."); + + let cleaners = listeners.get(window); + if (!cleaners) { + cleaners = []; + listeners.set(window, cleaners); + + // We need to remove from our map the `window` once is closed, to prevent + // memory leak + ShimWaiver.getProperty(window, "addEventListener")("DOMWindowClose", removeFromListeners); + } + + cleaners.push(() => ShimWaiver.getProperty(target, "removeEventListener")(type, listener, capture)); + ShimWaiver.getProperty(target, "addEventListener")(type, listener, capture); + + return output; +} + +unload(() => { + let keys = ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(listeners) + for (let window of keys) + removeFromListeners.call(window); +}); + +exports.open = open; diff --git a/toolkit/jetpack/sdk/event/target.js b/toolkit/jetpack/sdk/event/target.js new file mode 100644 index 000000000..3a1f5e5f0 --- /dev/null +++ b/toolkit/jetpack/sdk/event/target.js @@ -0,0 +1,74 @@ +/* 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": "stable" +}; + +const { on, once, off, setListeners } = require('./core'); +const { method, chainable } = require('../lang/functional/core'); +const { Class } = require('../core/heritage'); + +/** + * `EventTarget` is an exemplar for creating an objects that can be used to + * add / remove event listeners on them. Events on these objects may be emitted + * via `emit` function exported by 'event/core' module. + */ +const EventTarget = Class({ + /** + * Method initializes `this` event source. It goes through properties of a + * given `options` and registers listeners for the ones that look like an + * event listeners. + */ + /** + * Method initializes `this` event source. It goes through properties of a + * given `options` and registers listeners for the ones that look like an + * event listeners. + */ + initialize: function initialize(options) { + setListeners(this, options); + }, + /** + * Registers an event `listener` that is called every time events of + * specified `type` are emitted. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + * @example + * worker.on('message', function (data) { + * console.log('data received: ' + data) + * }) + */ + on: chainable(method(on)), + /** + * Registers an event `listener` that is called once the next time an event + * of the specified `type` is emitted. + * @param {String} type + * The type of the event. + * @param {Function} listener + * The listener function that processes the event. + */ + once: chainable(method(once)), + /** + * Removes an event `listener` for the given event `type`. + * @param {String} type + * The type of event. + * @param {Function} listener + * The listener function that processes the event. + */ + removeListener: function removeListener(type, listener) { + // Note: We can't just wrap `off` in `method` as we do it for other methods + // cause skipping a second or third argument will behave very differently + // than intended. This way we make sure all arguments are passed and only + // one listener is removed at most. + off(this, type, listener); + return this; + }, + // but we can wrap `off` here, as the semantics are the same + off: chainable(method(off)) + +}); +exports.EventTarget = EventTarget; diff --git a/toolkit/jetpack/sdk/event/utils.js b/toolkit/jetpack/sdk/event/utils.js new file mode 100644 index 000000000..f193b6785 --- /dev/null +++ b/toolkit/jetpack/sdk/event/utils.js @@ -0,0 +1,328 @@ +/* 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" +}; + +var { emit, on, once, off, EVENT_TYPE_PATTERN } = require("./core"); +const { Cu } = require("chrome"); + +// 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 +// more details. +var refs = (function() { + let refSets = new WeakMap(); + return function refs(target) { + if (!refSets.has(target)) refSets.set(target, new Set()); + return refSets.get(target); + }; +})(); + +function transform(input, f) { + let output = new Output(); + + // Since event listeners don't prevent `input` to be GC-ed we wanna presrve + // it until `output` can be GC-ed. There for we add implicit reference which + // is removed once `input` ends. + refs(output).add(input); + + const next = data => receive(output, data); + once(output, "start", () => start(input)); + on(input, "error", error => emit(output, "error", error)); + on(input, "end", function() { + refs(output).delete(input); + end(output); + }); + on(input, "data", data => f(data, next)); + return output; +} + +// High order event transformation function that takes `input` event channel +// and returns transformation containing only events on which `p` predicate +// returns `true`. +function filter(input, predicate) { + return transform(input, function(data, next) { + if (predicate(data)) + next(data); + }); +} +exports.filter = filter; + +// High order function that takes `input` and returns input of it's values +// mapped via given `f` function. +const map = (input, f) => transform(input, (data, next) => next(f(data))); +exports.map = map; + +// High order function that takes `input` stream of streams and merges them +// into single event stream. Like flatten but time based rather than order +// based. +function merge(inputs) { + let output = new Output(); + let open = 1; + let state = []; + output.state = state; + refs(output).add(inputs); + + function end(input) { + open = open - 1; + refs(output).delete(input); + if (open === 0) emit(output, "end"); + } + const error = e => emit(output, "error", e); + function forward(input) { + state.push(input); + open = open + 1; + on(input, "end", () => end(input)); + on(input, "error", error); + on(input, "data", data => emit(output, "data", data)); + } + + // If `inputs` is an array treat it as a stream. + if (Array.isArray(inputs)) { + inputs.forEach(forward); + end(inputs); + } + else { + on(inputs, "end", () => end(inputs)); + on(inputs, "error", error); + on(inputs, "data", forward); + } + + return output; +} +exports.merge = merge; + +const expand = (inputs, f) => merge(map(inputs, f)); +exports.expand = expand; + +const pipe = (from, to) => on(from, "*", emit.bind(emit, to)); +exports.pipe = pipe; + + +// Shim signal APIs so other modules can be used as is. +const receive = (input, message) => { + if (input[receive]) + input[receive](input, message); + else + emit(input, "data", message); + + // Ideally our input will extend Input and already provide a weak value + // getter. If not, opportunistically shim the weak value getter on + // other types passed as the input. + if (!("value" in input)) { + Object.defineProperty(input, "value", WeakValueGetterSetter); + } + input.value = message; +}; +receive.toString = () => "@@receive"; +exports.receive = receive; +exports.send = receive; + +const end = input => { + if (input[end]) + input[end](input); + else + emit(input, "end", input); +}; +end.toString = () => "@@end"; +exports.end = end; + +const stop = input => { + if (input[stop]) + input[stop](input); + else + emit(input, "stop", input); +}; +stop.toString = () => "@@stop"; +exports.stop = stop; + +const start = input => { + if (input[start]) + input[start](input); + else + emit(input, "start", input); +}; +start.toString = () => "@@start"; +exports.start = start; + +const lift = (step, ...inputs) => { + let args = null; + let opened = inputs.length; + let started = false; + const output = new Output(); + const init = () => { + args = [...inputs.map(input => input.value)]; + output.value = step(...args); + }; + + inputs.forEach((input, index) => { + on(input, "data", data => { + args[index] = data; + receive(output, step(...args)); + }); + on(input, "end", () => { + opened = opened - 1; + if (opened <= 0) + end(output); + }); + }); + + once(output, "start", () => { + inputs.forEach(start); + init(); + }); + + init(); + + return output; +}; +exports.lift = lift; + +const merges = inputs => { + let opened = inputs.length; + let output = new Output(); + output.value = inputs[0].value; + inputs.forEach((input, index) => { + on(input, "data", data => receive(output, data)); + on(input, "end", () => { + opened = opened - 1; + if (opened <= 0) + end(output); + }); + }); + + once(output, "start", () => { + inputs.forEach(start); + output.value = inputs[0].value; + }); + + return output; +}; +exports.merges = merges; + +const foldp = (step, initial, input) => { + let output = map(input, x => step(output.value, x)); + output.value = initial; + return output; +}; +exports.foldp = foldp; + +const keepIf = (p, base, input) => { + let output = filter(input, p); + output.value = base; + return output; +}; +exports.keepIf = keepIf; + +function Input() {} +Input.start = input => emit(input, "start", input); +Input.prototype.start = Input.start; + +Input.end = input => { + emit(input, "end", input); + stop(input); +}; +Input.prototype[end] = Input.end; + +// The event channel system caches the last event seen as input.value. +// Unfortunately, if the last event is a DOM object this is a great way +// leak windows. Mitigate this by storing input.value using a weak +// reference. This allows the system to work for normal event processing +// while also allowing the objects to be reclaimed. It means, however, +// input.value cannot be accessed long after the event was dispatched. +const WeakValueGetterSetter = { + get: function() { + return this._weakValue ? this._weakValue.get() : this._simpleValue + }, + set: function(v) { + if (v && typeof v === "object") { + try { + // Try to set a weak reference. This can throw for some values. + // For example, if the value is a native object that does not + // implement nsISupportsWeakReference. + this._weakValue = Cu.getWeakReference(v) + this._simpleValue = undefined; + return; + } catch (e) { + // Do nothing. Fall through to setting _simpleValue below. + } + } + this._simpleValue = v; + this._weakValue = undefined; + }, +} +Object.defineProperty(Input.prototype, "value", WeakValueGetterSetter); + +exports.Input = Input; + +// Define an Output type with a weak value getter for the transformation +// functions that produce new channels. +function Output() { } +Object.defineProperty(Output.prototype, "value", WeakValueGetterSetter); +exports.Output = Output; + +const $source = "@@source"; +const $outputs = "@@outputs"; +exports.outputs = $outputs; + +// NOTE: Passing DOM objects through a Reactor can cause them to leak +// when they get cached in this.value. We cannot use a weak reference +// in this case because the Reactor design expects to always have both the +// past and present value. If we allow past values to be collected the +// system breaks. + +function Reactor(options={}) { + const {onStep, onStart, onEnd} = options; + if (onStep) + this.onStep = onStep; + if (onStart) + this.onStart = onStart; + if (onEnd) + this.onEnd = onEnd; +} +Reactor.prototype.onStep = _ => void(0); +Reactor.prototype.onStart = _ => void(0); +Reactor.prototype.onEnd = _ => void(0); +Reactor.prototype.onNext = function(present, past) { + this.value = present; + this.onStep(present, past); +}; +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; + +const when = (target, type) => new Promise(resolve => { + once(target, type, resolve); +}); +exports.when = when; |