From edfe18f17cea4239fa72ebc46383dbb779ef54e8 Mon Sep 17 00:00:00 2001 From: hij1nx Date: Mon, 1 Aug 2011 13:09:24 -0400 Subject: [PATCH 1/2] [lib] events update, adds propper namespacing and wildcard support --- lib/events.js | 399 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 317 insertions(+), 82 deletions(-) diff --git a/lib/events.js b/lib/events.js index 675693ff705f..370476387657 100644 --- a/lib/events.js +++ b/lib/events.js @@ -19,31 +19,195 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. -var isArray = Array.isArray; +var EventEmitter = function EventEmitter(conf) { + this.init(); + this.setConf(conf); +}; -function EventEmitter() { } exports.EventEmitter = EventEmitter; +EventEmitter.prototype.init = function() { + this._events = new Object; // faster in v8 + this.defaultMaxListeners = 10; +}; + +var isArray = Array.isArray; + // By default EventEmitters will print a warning if more than // 10 listeners are added to it. This is a useful default which // helps finding memory leaks. // // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. -var defaultMaxListeners = 10; + +EventEmitter.prototype.setConf = function(conf) { + this.wildcard = conf && conf.wildcard; + this.delimiter = conf && conf.delimiter || '.'; + if(this.wildcard) { + this.listenerTree = new Object; + } +}; + EventEmitter.prototype.setMaxListeners = function(n) { - if (!this._events) this._events = {}; + this._events || this.init(); this._events.maxListeners = n; }; +EventEmitter.prototype.event = ''; + +var searchListenerTree = function(handlers, type, tree, i) { + if (!tree) { + return + } + + var listeners; + + if (i === type.length && tree._listeners) { + // + // If at the end of the event(s) list and the tree has listeners + // invoke those listeners. + // + if (typeof tree._listeners === 'function') { + handlers && handlers.push(tree._listeners); + return tree; + } else { + for (var leaf = 0, len = tree._listeners.length; leaf < len; leaf++) { + handlers && handlers.push(tree._listeners[leaf]); + } + return tree; + } + } + + if (type[i] === '*' || tree[type[i]]) { + // + // If the event emitted is '*' at this part + // or there is a concrete match at this patch + // + if (type[i] === '*') { + for (var branch in tree) { + if (branch !== '_listeners' && tree.hasOwnProperty(branch)) { + listeners = searchListenerTree(handlers, type, tree[branch], i+1); + } + } + return listeners; + } + + listeners = searchListenerTree(handlers, type, tree[type[i]], i+1); + } + + + if (tree['*']) { + // + // If the listener tree will allow any match for this part, + // then recursively explore all branches of the tree + // + searchListenerTree(handlers, type, tree['*'], i+1); + } + + return listeners; +}; + +var growListenerTree = function(type, listener) { + + type = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + + var tree = this.listenerTree; + var name = type.shift(); + + while (name) { + + if (!tree[name]) { + tree[name] = new Object; + } + + tree = tree[name]; + + if (type.length === 0) { + + if (!tree._listeners) { + tree._listeners = listener; + } + else if(typeof tree._listeners === 'function') { + tree._listeners = [tree._listeners, listener]; + } + else if (isArray(tree._listeners)) { + + tree._listeners.push(listener); + + if (!tree._listeners.warned) { + + var m = this.defaultMaxListeners; + + if (m > 0 && tree._listeners.length > m) { + + tree._listeners.warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + tree._listeners.length); + console.trace(); + } + } + } + return true; + } + name = type.shift(); + } + return true; +}; + +EventEmitter.prototype.once = function(event, fn) { + this.many(event, 1, fn); + return this; +}; + +EventEmitter.prototype.many = function(event, ttl, fn) { + var self = this; + + if (typeof fn !== 'function') { + throw new Error('many only accepts instances of Function'); + } + + function listener() { + if (--ttl === 0) { + self.un(event, listener); + } + fn.apply(null, arguments); + }; + + listener._origin = fn; + + this.on(event, listener); + + return self; +}; EventEmitter.prototype.emit = function() { + this._events || this.init(); + var type = arguments[0]; + this.event = type; + // If there is no 'error' event listener then throw. + + if (type === 'newListener') { + if (!this._events.newListener) { return false; } + } + + // Loop through the *_allListenerFn* functions and invoke them. + if (this._all) { + var l = arguments.length; + var args = new Array(l - 1); + for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; + for (i = 0, l = this._all.length; i < l; i++) { + this._all[i].apply(this, args); + } + } + if (type === 'error') { - if (!this._events || !this._events.error || - (isArray(this._events.error) && !this._events.error.length)) - { + if (!this._events.error || typeof this._events.error !== 'function' && + typeof this._events.error[0] !== 'function') { + if (arguments[1] instanceof Error) { throw arguments[1]; // Unhandled 'error' event } else { @@ -53,32 +217,39 @@ EventEmitter.prototype.emit = function() { } } - if (!this._events) return false; - var handler = this._events[type]; - if (!handler) return false; + var handler; - if (typeof handler == 'function') { - switch (arguments.length) { - // fast cases - case 1: - handler.call(this); - break; - case 2: - handler.call(this, arguments[1]); - break; - case 3: - handler.call(this, arguments[1], arguments[2]); - break; - // slower - default: - var l = arguments.length; - var args = new Array(l - 1); - for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; - handler.apply(this, args); + if(this.wildcard) { + handler = []; + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + searchListenerTree.call(this, handler, ns, this.listenerTree, 0); + } + else { + handler = this._events[type]; + } + + if (typeof handler === 'function') { + if (arguments.length === 1) { + handler.call(this); } + else if (arguments.length > 1) + switch (arguments.length) { + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + var l = arguments.length; + var args = new Array(l - 1); + for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; + handler.apply(this, args); + } return true; - - } else if (isArray(handler)) { + } + else if (handler) { var l = arguments.length; var args = new Array(l - 1); for (var i = 1; i < l; i++) args[i - 1] = arguments[i]; @@ -88,43 +259,46 @@ EventEmitter.prototype.emit = function() { listeners[i].apply(this, args); } return true; - - } else { - return false; } -}; -// EventEmitter is defined in src/node_events.cc -// EventEmitter.prototype.emit() is also defined there. -EventEmitter.prototype.addListener = function(type, listener) { - if ('function' !== typeof listener) { - throw new Error('addListener only takes instances of Function'); - } +}; - if (!this._events) this._events = {}; +EventEmitter.prototype.on = function(type, listener) { + this._events || this.init(); // To avoid recursion in the case that type == "newListeners"! Before // adding it to the listeners, first emit "newListeners". this.emit('newListener', type, listener); + if(this.wildcard) { + growListenerTree.call(this, type, listener); + return this; + } + if (!this._events[type]) { // Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener; - } else if (isArray(this._events[type])) { - + } + else if(typeof this._events[type] === 'function') { + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + } + else if (isArray(this._events[type])) { // If we've already got an array, just append. this._events[type].push(listener); // Check for listener leak if (!this._events[type].warned) { + var m; if (this._events.maxListeners !== undefined) { m = this._events.maxListeners; } else { - m = defaultMaxListeners; + m = this.defaultMaxListeners; } if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' + @@ -133,80 +307,141 @@ EventEmitter.prototype.addListener = function(type, listener) { console.trace(); } } - } else { - // Adding the second element, need to change to array. - this._events[type] = [this._events[type], listener]; } - return this; }; -EventEmitter.prototype.on = EventEmitter.prototype.addListener; +EventEmitter.prototype.onAny = function(fn) { -EventEmitter.prototype.once = function(type, listener) { - if ('function' !== typeof listener) { - throw new Error('.once only takes instances of Function'); + if(!this._all) { + this._all = []; } - var self = this; - function g() { - self.removeListener(type, g); - listener.apply(this, arguments); - }; - - g.listener = listener; - self.on(type, g); + if (typeof fn !== 'function') { + throw new Error('onAny only accepts instances of Function'); + } - return this; + // Add the function to the event listener collection. + this._all.push(fn); }; -EventEmitter.prototype.removeListener = function(type, listener) { - if ('function' !== typeof listener) { +EventEmitter.prototype.addListener = EventEmitter.prototype.on; + +EventEmitter.prototype.un = function(type, listener) { + if (typeof listener !== 'function') { throw new Error('removeListener only takes instances of Function'); } - // does not use listeners(), so no side effect of creating _events[type] - if (!this._events || !this._events[type]) return this; + var handlers; - var list = this._events[type]; + if(this.wildcard) { + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + var leaf = searchListenerTree.call(this, null, ns, this.listenerTree, 0); + + if('undefined' === typeof leaf) { return this; } + handlers = leaf._listeners; + } + else { + // does not use listeners(), so no side effect of creating _events[type] + if (!this._events[type]) return this; + handlers = this._events[type]; + } + + if (isArray(handlers)) { - if (isArray(list)) { var position = -1; - for (var i = 0, length = list.length; i < length; i++) { - if (list[i] === listener || - (list[i].listener && list[i].listener === listener)) - { + + for (var i = 0, length = handlers.length; i < length; i++) { + if (handlers[i] === listener || + (handlers[i].listener && handlers[i].listener === listener) || + (handlers[i]._origin && handlers[i]._origin === listener)) { position = i; break; } } - if (position < 0) return this; - list.splice(position, 1); - if (list.length == 0) + if (position < 0) { + return this; + } + + if(this.wildcard) { + leaf._listeners.splice(position, 1) + } + else { + this._events[type].splice(position, 1); + } + + if (handlers.length === 0) { + if(this.wildcard) { + delete leaf._listeners; + } + else { + delete this._events[type]; + } + } + } + else if (handlers === listener || + (handlers.listener && handlers.listener === listener) || + (handlers._origin && handlers._origin === listener)) { + if(this.wildcard) { + delete leaf._listeners; + } + else { delete this._events[type]; - } else if (list === listener || - (list.listener && list.listener === listener)) - { - delete this._events[type]; + } } return this; }; +EventEmitter.prototype.unAny = function(fn) { + var i = 0, l = 0, fns; + if (fn && this._all && this._all.length > 0) { + fns = this._all; + for(i = 0, l = fns.length; i < l; i++) { + if(fn === fns[i]) { + fns.splice(i, 1); + return this; + } + } + } else { + this._all = []; + } + return this; +}; + +EventEmitter.prototype.removeListener = EventEmitter.prototype.un; + EventEmitter.prototype.removeAllListeners = function(type) { if (arguments.length === 0) { - this._events = {}; + !this._events || this.init(); return this; } - // does not use listeners(), so no side effect of creating _events[type] - if (type && this._events && this._events[type]) this._events[type] = null; + if(this.wildcard) { + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + var leaf = searchListenerTree.call(this, null, ns, this.listenerTree, 0); + + if('undefined' === typeof leaf) { return this; } + leaf._listeners = null; + } + else { + if (!this._events[type]) return this; + this._events[type] = null; + } return this; }; EventEmitter.prototype.listeners = function(type) { - if (!this._events) this._events = {}; + if(this.wildcard) { + var handlers = []; + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + searchListenerTree.call(this, handlers, ns, this.listenerTree, 0); + return handlers; + } + + this._events || this.init(); + if (!this._events[type]) this._events[type] = []; if (!isArray(this._events[type])) { this._events[type] = [this._events[type]]; From 189ebb53e644cb97b917512b6643c0d5b08ff1ee Mon Sep 17 00:00:00 2001 From: hij1nx Date: Mon, 1 Aug 2011 21:13:53 -0400 Subject: [PATCH 2/2] [doc/api] updated docs to reflect the changes to events.js --- doc/api/events.markdown | 92 ++++++++++++++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/doc/api/events.markdown b/doc/api/events.markdown index cf1ec9216b91..494251b847e5 100644 --- a/doc/api/events.markdown +++ b/doc/api/events.markdown @@ -12,6 +12,24 @@ Functions can then be attached to objects, to be executed when an event is emitted. These functions are called _listeners_. +**Namespaces** with **Wildcards** +To use namespaces/wildcards, pass the `wildcard` option into the EventEmitter constructor. +When namespaces/wildcards are enabled, events can either be strings (`foo.bar`) separated +by a delimiter or arrays (`['foo', 'bar']`). The delimiter is also configurable as a +constructor option. + +An event name passed to any event emitter method can contain a wild card (the `*` character). +If the event name is a string, a wildcard may appear as `foo.*`. If the event name is an array, +the wildcard may appear as `['foo', '*']`. + +If either of the above described events were passed to the `on` method, subsequent emits such +as the following would be observed... + + + emitter.emit(['foo.bazz']); + emitter.emit(['foo', 'bar']); + + ### events.EventEmitter To access the EventEmitter class, `require('events').EventEmitter`. @@ -29,31 +47,63 @@ added. Adds a listener to the end of the listeners array for the specified event. + server.on('connection', function (stream) { console.log('someone connected!'); }); + +#### emitter.onAny(listener) + +Adds a listener that will be fired when any event is emitted. + + function f(value) { + console.log('This event will be listened to exactly four times.'); + }; + + server.onAny(f); + + +#### emitter.unAny(listener) + +Removes the listener that will be fired when any event is emitted. + + + server.unAny(f); + + #### emitter.once(event, listener) -Adds a **one time** listener for the event. The listener is -invoked only the first time the event is fired, after which -it is removed. +Adds a **one time** listener for the event. The listener is invoked only the first time the event is fired, after which it is removed. - server.once('connection', function (stream) { - console.log('Ah, we have our first user!'); + + server.once('connection', function (value) { + console.log('Ah, we have our first value!'); + }); + + +#### emitter.many(event, timesToListen, listener) + +Adds a listener that will execute **n times** for the event before being removed. The listener is invoked only the first time the event is fired, after which it is removed. + + + server.many('connection', 4, function (value) { + console.log('Ah, we have captured a user!'); }); + #### emitter.removeListener(event, listener) +#### emitter.un(event, listener) + +Remove a listener from the listener array for the specified event. **Caution**: changes array indices in the listener array behind the listener. -Remove a listener from the listener array for the specified event. -**Caution**: changes array indices in the listener array behind the listener. var callback = function(stream) { console.log('someone connected!'); }; server.on('connection', callback); // ... - server.removeListener('connection', callback); + server.un('connection', callback); #### emitter.removeAllListeners([event]) @@ -63,28 +113,40 @@ Removes all listeners, or those of the specified event. #### emitter.setMaxListeners(n) -By default EventEmitters will print a warning if more than 10 listeners are -added to it. This is a useful default which helps finding memory leaks. -Obviously not all Emitters should be limited to 10. This function allows -that to be increased. Set to zero for unlimited. +By default EventEmitters will print a warning if more than 10 listeners are added to it. This is a useful default which helps finding memory leaks. Obviously not all Emitters should be limited to 10. This function allows that to be increased. Set to zero for unlimited. #### emitter.listeners(event) -Returns an array of listeners for the specified event. This array can be -manipulated, e.g. to remove listeners. +Returns an array of listeners for the specified event. This array can be manipulated, e.g. to remove listeners. + server.on('connection', function (stream) { console.log('someone connected!'); }); + console.log(util.inspect(server.listeners('connection')); // [ [Function] ] + +#### emitter.listenersAny(event) + +Returns an array of listeners that are listening for any event that is specified. This array can be manipulated, e.g. to remove listeners. + + + server.onAny(function(value) { + console.log('someone connected!'); + }); + + console.log(console.log(server.listenersAny()[0]); // [ [Function] ] // someone connected! + + #### emitter.emit(event, [arg1], [arg2], [...]) -Execute each of the listeners in order with the supplied arguments. +Execute each of the listeners that may be listening for the specified event name in order with the list of arguments. #### Event: 'newListener' `function (event, listener) { }` This event is emitted any time someone adds a new listener. +