Prohibited duplicate listeners of the same event

events
Mike Gerwitz 2014-08-03 00:10:01 -04:00
parent 875c43cee4
commit 66e3d5b1ef
2 changed files with 65 additions and 5 deletions

View File

@ -193,6 +193,12 @@ module.exports = Trait( 'Evented',
else if ( typeof listener !== 'function' ) {
throw TypeError( "Event listener must be a function" );
}
else if ( this._hooksEvent( ev, listener ) )
{
throw Error(
`Listener has already been bound to event \`${ev}'`
);
}
this.hookEvent( ev, listener );
return this;
@ -222,12 +228,17 @@ module.exports = Trait( 'Evented',
let evls = this._events[ ev ],
avail = this._gaps[ ev ].pop();
if ( listener[ _evid ] === undefined )
{
listener[ _evid ] = {};
}
if ( avail === undefined ) {
listener[ _evid ] = evls.length;
listener[ _evid ][ ev ] = evls.length;
evls.push( listener );
}
else {
listener[ _evid ] = avail;
listener[ _evid ][ ev ] = avail;
evls[ avail ] = listener;
}
@ -235,6 +246,27 @@ module.exports = Trait( 'Evented',
},
/**
* Determine whether LISTENER is actively monitoring event EV
*
* Note that this method does not verify by reference that the given
* listener is legitimate---it assumes that the event metadata attached
* to LISTENER (if any) has not been tampered with or forged.
*
* @O {1} constant time
*
* @param {string} ev defined event id
* @param {Function} listener listener to query
*
* @return {boolean} whether LISTENER hooks event EV
*/
_hooksEvent( ev, listener )
{
let levdata = listener[ _evid ];
return ( levdata && ( levdata[ ev ] !== undefined ) );
},
/**
* Alias for #on
*
@ -274,15 +306,18 @@ module.exports = Trait( 'Evented',
throw Error( `Cannot unhook undefined event \`${ev}'` );
}
let evls = this._events[ ev ],
index = listener[ _evid ];
let evls = this._events[ ev ],
levdata = listener[ _evid ] || {},
index = levdata[ ev ];
// this is important, since we (a) cannot necessarily trust that the
// cached index hasn't been forged and (b) if a listener is removed
// multiple times, the index may contain a different listener
if ( evls[ index ] === listener )
if ( ( index !== undefined ) && ( evls[ index ] === listener ) )
{
evls[ index ] = undefined;
levdata[ ev ] = undefined;
this._gaps[ ev ].push( index );
}

View File

@ -189,6 +189,31 @@ describe( 'event.Evented', () =>
expect( () => stub[ on ]( ev, "kittens" ) )
.to.throw( TypeError );
} );
/**
* It is highly suspect if the exact same listener is registered
* for the same event---it is either a bug, or a questionable
* design decision; we assume that the former is far more likely
* to be true, and that the latter should be addressed and
* resolved.
*
* Note that this does not prevent a caller from wrapping a
* listener in a function that is instead registered as the
* listener---this is perfectly valid and has many use cases.
*/
it( 'does not permit same event listener multiple times', () =>
{
var ev = 'foo',
f = () => {};
expect( () =>
{
stub.evDefineEvents( [ ev ] )
[ on ]( ev, f )
[ on ]( ev, f );
} ).to.throw( Error );
} );
} );
} );