Evented trait with basic functionality
This provides the core of the trait, with support for event definitions and hooks.events
parent
0d6a2fc652
commit
2942f153db
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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.<string>} 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.<string>} 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.<Function>} 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;
|
||||
}
|
||||
} );
|
||||
|
|
@ -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]'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
Loading…
Reference in New Issue