Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
167 lines
4.7 KiB
JavaScript
Executable file
167 lines
4.7 KiB
JavaScript
Executable file
/*
|
|
* Custom events lib. Notable features:
|
|
*
|
|
* - the module itself is an event emitter. Useful for "global" pub/sub.
|
|
* - evt.mix can be used to mix in an event emitter into existing object.
|
|
* - notification of listeners is done in a try/catch, so all listeners
|
|
* are notified even if one fails. Errors are thrown async via setTimeout
|
|
* so that all the listeners can be notified without escaping from the
|
|
* code via a throw within the listener group notification.
|
|
* - new evt.Emitter() can be used to create a new instance of an
|
|
* event emitter.
|
|
* - Uses "this" internally, so always call object with the emitter args.
|
|
*/
|
|
'use strict';
|
|
define(function() {
|
|
|
|
var evt,
|
|
slice = Array.prototype.slice,
|
|
props = ['_events', '_pendingEvents', 'on', 'once', 'latest',
|
|
'latestOnce', 'removeListener', 'emitWhenListener', 'emit'];
|
|
|
|
function Emitter() {
|
|
this._events = {};
|
|
this._pendingEvents = {};
|
|
}
|
|
|
|
Emitter.prototype = {
|
|
on: function(id, fn) {
|
|
var listeners = this._events[id],
|
|
pending = this._pendingEvents[id];
|
|
if (!listeners) {
|
|
listeners = this._events[id] = [];
|
|
}
|
|
listeners.push(fn);
|
|
|
|
if (pending) {
|
|
pending.forEach(function(args) {
|
|
fn.apply(null, args);
|
|
});
|
|
delete this._pendingEvents[id];
|
|
}
|
|
return this;
|
|
},
|
|
|
|
once: function(id, fn) {
|
|
var self = this,
|
|
fired = false;
|
|
function one() {
|
|
if (fired) {
|
|
return;
|
|
}
|
|
fired = true;
|
|
fn.apply(null, arguments);
|
|
// Remove at a further turn so that the event
|
|
// forEach in emit does not get modified during
|
|
// this turn.
|
|
setTimeout(function() {
|
|
self.removeListener(id, one);
|
|
});
|
|
}
|
|
return this.on(id, one);
|
|
},
|
|
|
|
/**
|
|
* Waits for a property on the object that has the event interface
|
|
* to be available. That property MUST EVALUATE TO A TRUTHY VALUE.
|
|
* hasOwnProperty is not used because many objects are created with
|
|
* null placeholders to give a proper JS engine shape to them, and
|
|
* this method should not trigger the listener for those cases.
|
|
* If the property is already available, call the listener right
|
|
* away. If not available right away, listens for an event name that
|
|
* matches the property name.
|
|
* @param {String} id property name.
|
|
* @param {Function} fn listener.
|
|
*/
|
|
latest: function(id, fn) {
|
|
if (this[id] && !this._pendingEvents[id]) {
|
|
fn(this[id]);
|
|
}
|
|
this.on(id, fn);
|
|
},
|
|
|
|
/**
|
|
* Same as latest, but only calls the listener once.
|
|
* @param {String} id property name.
|
|
* @param {Function} fn listener.
|
|
*/
|
|
latestOnce: function(id, fn) {
|
|
if (this[id] && !this._pendingEvents[id]) {
|
|
fn(this[id]);
|
|
} else {
|
|
this.once(id, fn);
|
|
}
|
|
},
|
|
|
|
removeListener: function(id, fn) {
|
|
var i,
|
|
listeners = this._events[id];
|
|
if (listeners) {
|
|
i = listeners.indexOf(fn);
|
|
if (i !== -1) {
|
|
listeners.splice(i, 1);
|
|
}
|
|
if (listeners.length === 0) {
|
|
delete this._events[id];
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Like emit, but if no listeners yet, holds on
|
|
* to the value until there is one. Any other
|
|
* args after first one are passed to listeners.
|
|
* @param {String} id event ID.
|
|
*/
|
|
emitWhenListener: function(id) {
|
|
var listeners = this._events[id];
|
|
if (listeners) {
|
|
this.emit.apply(this, arguments);
|
|
} else {
|
|
if (!this._pendingEvents[id]) {
|
|
this._pendingEvents[id] = [];
|
|
}
|
|
this._pendingEvents[id].push(slice.call(arguments, 1));
|
|
}
|
|
},
|
|
|
|
emit: function(id) {
|
|
var args = slice.call(arguments, 1),
|
|
listeners = this._events[id];
|
|
|
|
if (listeners) {
|
|
listeners.forEach(function(fn) {
|
|
try {
|
|
fn.apply(null, args);
|
|
} catch (e) {
|
|
// Throw at later turn so that other listeners
|
|
// can complete. While this messes with the
|
|
// stack for the error, continued operation is
|
|
// valued more in this tradeoff.
|
|
// This also means we do not need to .catch()
|
|
// for the wrapping promise.
|
|
setTimeout(function() {
|
|
throw e;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
evt = new Emitter();
|
|
evt.Emitter = Emitter;
|
|
|
|
evt.mix = function(obj) {
|
|
var e = new Emitter();
|
|
props.forEach(function(prop) {
|
|
if (obj.hasOwnProperty(prop)) {
|
|
throw new Error('Object already has a property "' + prop + '"');
|
|
}
|
|
obj[prop] = e[prop];
|
|
});
|
|
return obj;
|
|
};
|
|
|
|
return evt;
|
|
});
|