summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/event
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/jetpack/sdk/event')
-rw-r--r--toolkit/jetpack/sdk/event/chrome.js65
-rw-r--r--toolkit/jetpack/sdk/event/core.js193
-rw-r--r--toolkit/jetpack/sdk/event/dom.js78
-rw-r--r--toolkit/jetpack/sdk/event/target.js74
-rw-r--r--toolkit/jetpack/sdk/event/utils.js328
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;