/* * 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; });