diff --git a/src/event/Evented.js b/src/event/Evented.js
new file mode 100644
index 0000000..61c8b3f
--- /dev/null
+++ b/src/event/Evented.js
@@ -0,0 +1,176 @@
+/**
+ * Trait providing 1-N message dispatch
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of jsTonic.
+ *
+ * jstonic is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var Trait = require( 'easejs' ).Trait,
+ isArray = require( '../std/Array' ).isArray;
+
+
+/**
+ * Flexible event system augmentation
+ *
+ * The class mixing in this trait will be endowed with a complete event
+ * system with which events may be registered, hooked, and emitted. The
+ * callback scheduler may be overridden to alter the algorithm for invoking
+ * callbacks.
+ *
+ * The API is motivated by (and is a replacement for) Node.js' event emitter
+ * implementation.
+ */
+module.exports = Trait( 'Evented',
+{
+ /**
+ * Registered events and their callbacks
+ * @type {Object}
+ */
+ _events: {},
+
+
+ /**
+ * Defines a list of events by unique identifier
+ *
+ * If any of the given identifiers is already in use (or if duplicates
+ * are provided), an error will be thrown; this is to prevent
+ * accidentally mixing callbacks when an event is assumed to have not
+ * already been defined.
+ *
+ * @param {Array.} ids list of event ids
+ * @return {Evented} self
+ *
+ * @throws {TypeError} if an array is not provided
+ * @throws {Error} on duplicate id conflict
+ */
+ 'protected defineEvents': function( ids )
+ {
+ if ( !isArray( ids ) ) {
+ throw TypeError( "Expected array of event ids" );
+ }
+
+ this._mergeDfn( ids );
+ return this;
+ },
+
+
+ /**
+ * Merges provided event ids into list of available events
+ *
+ * If an id is already defined, an exception will be thrown.
+ *
+ * @param {Array.} ids event ids
+ * @return {undefined}
+ *
+ * @throws {Error} on duplicate id definition
+ */
+ _mergeDfn: function( ids )
+ {
+ var i = ids.length;
+ while ( i-- ) {
+ var id = ids[ i ];
+
+ if ( this._events[ id ] ) {
+ throw Error( "Duplicate definition of event `" + id + "'" );
+ }
+
+ // will contain each callback associated with this event
+ this._events[ ids[ i ] ] = [];
+ }
+ },
+
+
+ /**
+ * Schedules all callbacks for defined event EV
+ *
+ * See documentation for the `scheduleCallbacks` method for information
+ * on the default callback scheduler.
+ *
+ * @param {string} ev defined event id
+ * @return {Evented} self
+ */
+ 'protected emit': function( ev )
+ {
+ if ( !( this._events[ ev ] ) ) {
+ throw Error( "Cannot emit undefined event `" + ev + "'" );
+ }
+
+ var args = [],
+ i = arguments.length;
+
+ // store args in a way that v8 can optimize
+ while ( --i ) {
+ args[ i - 1 ] = arguments[ i ];
+ }
+
+ this.scheduleCallbacks( ev, this._events[ ev ], args );
+ return this;
+ },
+
+
+ /**
+ * Invoke all callbacks for the given event
+ *
+ * The default scheduler invokes the callbacks synchronously and in an
+ * undefined order (that is, the implementation may change at any time);
+ * this method can be overridden to provide a different scheduler that
+ * provides different guarantees.
+ *
+ * @param {string} ev defined event id
+ * @param {Array.} evc event callbacks (hooks)
+ * @param {Array} args argument list to apply to callbacks
+ *
+ * @return {Evented} self
+ */
+ 'virtual protected scheduleCallbacks': function( ev, evc, args )
+ {
+ var i = evc.length;
+
+ while ( i-- ) {
+ evc[ i ].apply( null, args );
+ }
+
+ return this;
+ },
+
+
+ /**
+ * Invoke callback when an event is emitted
+ *
+ * The event EV must be defined and CALLBACK must be a function. The
+ * context in which the callback is invoked (the value of `this') is
+ * undefined.
+ *
+ * @param {string} ev defined event id
+ * @param {Function} callback function to invoke when event is emitted
+ *
+ * @return {Evented} self
+ */
+ on: function( ev, callback )
+ {
+ if ( !( this._events[ ev ] ) ) {
+ throw Error( "Cannot hook undefined event `" + ev + "'" );
+ }
+ else if ( typeof callback !== 'function' ) {
+ throw TypeError( "Event callback must be a function" );
+ }
+
+ this._events[ ev ].push( callback );
+ return this;
+ }
+} );
+
diff --git a/src/std/Array.js b/src/std/Array.js
index bd3eed2..2b8b833 100644
--- a/src/std/Array.js
+++ b/src/std/Array.js
@@ -40,7 +40,9 @@ module.exports = {
*/
_isArray: function( arr )
{
- return ( Object.prototype.toString.call( arr ) === '[object Array]' );
+ return (
+ Object.prototype.toString.call( arr ) === '[object Array]'
+ );
},
};
diff --git a/test/event/EventedTest.js b/test/event/EventedTest.js
new file mode 100644
index 0000000..2b88498
--- /dev/null
+++ b/test/event/EventedTest.js
@@ -0,0 +1,391 @@
+/**
+ * Tests trait providing 1-N message dispatch
+ *
+ * Copyright (C) 2014 Mike Gerwitz
+ *
+ * This file is part of jsTonic.
+ *
+ * jstonic is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+var Sut = require( '../../' ).event.Evented,
+ Class = require( 'easejs' ).Class,
+ expect = require( 'chai' ).expect,
+ common = require( '../lib' ),
+
+ EvStub = Class.use( Sut ).extend(
+ {
+ // defineEvents is protected
+ evDefineEvents: function( ids )
+ {
+ return this.defineEvents( ids );
+ },
+
+ // emit is protected
+ evEmit: function( ev )
+ {
+ return this.emit.apply( this, arguments );
+ }
+ } );
+
+
+describe( 'event.Evented', function()
+{
+ var stub;
+
+ beforeEach( function()
+ {
+ stub = EvStub();
+ } );
+
+
+ describe( '#defineEvents', function()
+ {
+ /**
+ * We aren't going to bother testing every possibly value; let's
+ * just see if the most common case (probably) is caught.
+ */
+ it( 'rejects non-array argument', function()
+ {
+ expect( function()
+ {
+ stub.evDefineEvents( 'string' );
+ } ).to.throw( TypeError, "array" );
+ } );
+
+
+ it( 'accepts array argument', function()
+ {
+ expect( function()
+ {
+ stub.evDefineEvents( [] );
+ } ).to.not.throw( TypeError );
+ } );
+
+
+ it( 'returns self', function()
+ {
+ expect( stub.evDefineEvents( [] ) ).to.equal( stub );
+ } );
+
+
+ /**
+ * This relies on functionality proven below
+ */
+ it( 'defines each event in provided list', function()
+ {
+ var events = [ 'a', 'b' ];
+ stub.evDefineEvents( events );
+
+ var i = events.length;
+ while ( i-- ) {
+ expect( function()
+ {
+ // will fail if event did not register
+ stub.on( events[ i ], function() {} );
+ } ).to.not.throw( Error );
+ }
+ } );
+
+
+ /**
+ * While at first glance this may seem like an odd thing to want to
+ * do, this will come in useful when exposed via an API that defines
+ * events individually.
+ */
+ it( 'merges definitions in multiple calls', function()
+ {
+ var evs = [ 'a', 'b' ];
+
+ // define each separately
+ for ( var i = 0; i < evs.length; i++ ) {
+ stub.evDefineEvents( [ evs[ i ] ] );
+
+ expect( function()
+ {
+ // will fail if event did not register
+ stub.on( evs[ i ], function() {} );
+ } ).to.not.throw( Error );
+ }
+ } );
+
+
+ /**
+ * We do not want someone to declare an event thinking that the id
+ * is free, which risks mixing handlers from what is supposed to be
+ * two separate events.
+ */
+ describe( 'throws exception on duplicate events', function()
+ {
+ it( 'in separate calls', function()
+ {
+ var name = 'ev';
+ stub.evDefineEvents( [ name ] );
+
+ expect( function()
+ {
+ stub.evDefineEvents( [ name ] );
+ } ).to.throw( Error, name );
+ } );
+
+
+ it( 'in a single call', function()
+ {
+ var dup = 'foo';
+
+ expect( function()
+ {
+ stub.evDefineEvents( [ 'a', dup, 'b', dup ] );
+ } ).to.throw( Error, dup );
+ } );
+ } );
+ } );
+
+
+ describe( '#on', function()
+ {
+ it( 'does not allow hooking undeclared events', function()
+ {
+ var badevent = 'bazbar';
+
+ expect( function()
+ {
+ stub.on( badevent, function() {} );
+ } ).to.throw( Error, badevent );
+ } );
+
+
+ it( 'allows hooking declared events', function()
+ {
+ var name = 'testev';
+ stub.evDefineEvents( [ name ] )
+ .on( name, function() {} );
+ } );
+
+
+ it( 'returns self', function()
+ {
+ var name = 'testev';
+
+ var ret = stub.evDefineEvents( [ name ] )
+ .on( name, function() {} );
+
+ expect( ret ).to.equal( stub );
+ } );
+
+
+ it( 'requires that callback is a function', function()
+ {
+ var ev = 'foo';
+ stub.evDefineEvents( [ ev ] );
+
+ // OK
+ expect( function()
+ {
+ stub.on( ev, function() {} );
+ } ).to.not.throw( TypeError );
+
+ // bad
+ expect( function()
+ {
+ stub.on( ev, "kittens" );
+ } ).to.throw( TypeError );
+ } );
+ } );
+
+
+ describe( '#emit', function()
+ {
+ it( 'cannot emit undefined events', function()
+ {
+ var ev = 'unknown';
+
+ expect( function()
+ {
+ stub.evEmit( ev );
+ } ).to.throw( Error, ev );
+ } );
+
+
+ it( 'invokes all callbacks attached with #on', function()
+ {
+ var ev = 'foo',
+ called = 0;
+
+ stub.evDefineEvents( [ ev ] )
+ .on( ev, function()
+ {
+ called++;
+ } );
+
+ expect( function()
+ {
+ stub.evEmit( ev );
+ } ).to.not.throw( Error );
+
+ // make sure the callback was invoked only once
+ expect( called ).to.equal( 1 );
+ } );
+
+
+ /**
+ * It is courteous for event handler to *not* modify the values they
+ * are given. Otherwise, the emitter must take care to clone or
+ * encapsulate the values if this is a concern.
+ */
+ it( 'passes all emit args by reference to callbacks', function( done )
+ {
+ var ev = 'foo',
+ a = {},
+ b = [];
+
+ stub.evDefineEvents( [ ev ] )
+ .on( ev, function( givena, givenb )
+ {
+ expect( givena ).to.equal( a );
+ expect( givenb ).to.equal( b );
+ done();
+ } )
+ .evEmit( ev, a, b );
+ } );
+
+
+ /**
+ * The default event scheduler synchronously invokes all callbacks
+ * in an undefined order (that is: the order may change in the
+ * future). This is not desirable for all cases, but that's not the
+ * point here.
+ */
+ it( 'invokes multiple callbacks synchronously', function()
+ {
+ var ev = 'foo',
+ called = [],
+ n = 2;
+
+ stub.evDefineEvents( [ ev ] );
+
+ // add N callbacks for EV
+ for ( var i = 0; i < n; i++ ) {
+ ( function( i )
+ {
+ stub.on( ev, function()
+ {
+ called[ i ] = true;
+ } );
+ } )( i );
+ }
+
+ // trigger callbacks
+ stub.evEmit( ev );
+
+ // by now, we care about two things: that all callbacks were
+ // invoked and that they have been invoked by this point,
+ // meaning that they were done synchronously
+ while ( n-- ) {
+ expect( called[ n ] ).to.equal( true );
+ }
+ } );
+
+
+ /**
+ * When we emit an event, only the callbacks for that specific event
+ * should be invoked.
+ */
+ it( 'does not mix callbacks from other events', function()
+ {
+ var ev = 'foo',
+ wrongev = 'bar';
+
+ stub.evDefineEvents( [ ev, wrongev ] );
+
+ var called = false;
+ stub.on( ev, function() { called = true; } );
+ stub.on( wrongev, function() { throw Error( "Thar be demons" ); } );
+
+ // wrongev callback should *not* be called
+ stub.evEmit( ev );
+ expect( called ).to.equal( true );
+ } );
+
+
+ /**
+ * It is common to leave events unhooked.
+ */
+ it( 'does not fail when emitting unhooked events', function()
+ {
+ var ev = 'foo';
+ expect( function()
+ {
+ // no hooks, but emit
+ stub.evDefineEvents( [ ev ] ).evEmit( ev );
+ } ).to.not.throw( Error );
+ } );
+
+
+ it( 'returns self', function()
+ {
+ var ev = 'foo';
+ expect( stub.evDefineEvents( [ ev ] ).evEmit( ev ) )
+ .to.equal( stub );
+ } );
+ } );
+
+
+ /**
+ * This is important to support stackable traits for custom schedulers.
+ */
+ describe( '#scheduleCallbacks', function()
+ {
+ var ev = 'foo',
+ a = 'bar',
+ b = 'baz';
+
+ it( 'can be overridden by subtypes', function( done )
+ {
+ common.checkOverride( EvStub, 'scheduleCallbacks',
+ function( given_ev, _, given_args )
+ {
+ expect( given_ev ).to.equal( ev );
+ expect( given_args ).to.deep.equal( [ a, b ] );
+ done();
+ }
+ )().evDefineEvents( [ ev ] ).evEmit( ev, a, b );
+ } );
+
+
+ /**
+ * The listeners are encapsulated within the trait, so they must be
+ * passed to us for processing.
+ */
+ it( 'is provided with callbacks to invoke', function( done )
+ {
+ var a = function() {},
+ b = function() {};
+
+ common.checkOverride( EvStub, 'scheduleCallbacks',
+ function( _, evc, __ )
+ {
+ expect( evc ).to.include( a );
+ expect( evc ).to.include( b );
+ done();
+ }
+ )()
+ .evDefineEvents( [ ev ] )
+ .on( ev, a )
+ .on( ev, b )
+ .evEmit( ev );
+ } );
+ } );
+} );
+