Eventable interface with extensive specification
parent
bac61c3f08
commit
7fa2a296d2
|
@ -0,0 +1,165 @@
|
||||||
|
/**
|
||||||
|
* 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 Interface = require( 'easejs' ).Interface;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface that provides basic, dynamic 1-N message dispatch using
|
||||||
|
* callbacks
|
||||||
|
*
|
||||||
|
* Each *message* is associated with an *event*---a string
|
||||||
|
* identifier---which is *emitted* by an Eventable object. A message
|
||||||
|
* *dispatched* to and *handled* by a *listener* that *hooks* (or *listens
|
||||||
|
* to*) an event. Details of when a message is emitted are left to an
|
||||||
|
* implementation; is therefore not guaranteed that all messages will be
|
||||||
|
* received by a listener after hooking an event, and a listener may or may
|
||||||
|
* not receive past messages upon hooking an event. A message is said to
|
||||||
|
* have been *delivered* to a listener when it has been successfully
|
||||||
|
* dispatched and, if required by an implementation, delivery confirmation
|
||||||
|
* has been provided by the listener.
|
||||||
|
*
|
||||||
|
* The contents of a message are its *arguments*, which are applied to the
|
||||||
|
* listener. The listener must therefore be a function, which may or may not
|
||||||
|
* be invoked synchronously.
|
||||||
|
*
|
||||||
|
* Strictly speaking, methods do not have to be dispatched 1-N---for
|
||||||
|
* example, a message may be emitted across multiple events---but this is
|
||||||
|
* the conventional use case, and consumers of Eventable objects should not
|
||||||
|
* assume otherwise unless the implementation documents such behavior.
|
||||||
|
*
|
||||||
|
* By leaving such details undefined, Eventable objects may be used in a
|
||||||
|
* variety of ways, such as a conventional Observer pattern (in which the
|
||||||
|
* subject is the Eventable object and the listeners are the observers) or
|
||||||
|
* even as a message queue (either directly, or as a proxy to place messages
|
||||||
|
* into a queue). Consumers of Eventable objects should make no assumptions
|
||||||
|
* as to its behavior unless they expect a certain implementation to be
|
||||||
|
* passed.
|
||||||
|
*
|
||||||
|
* As such, this interface declare only a public API; it does not even
|
||||||
|
* describe a method for emitting events; common names for such a method are
|
||||||
|
* `emit` (e.g. Node.js) and `notify` (e.g. the Observer pattern).
|
||||||
|
*
|
||||||
|
* Where implementation details below are "undefined", an implementation may
|
||||||
|
* choose its own behavior (not to be confused with the primitive ECMAScript
|
||||||
|
* type `undefined`).
|
||||||
|
*/
|
||||||
|
module.exports = Interface( 'Eventable',
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Invoke LISTENER with arguments of messages emitted by event EV
|
||||||
|
*
|
||||||
|
* The event EV must be valid and LISTENER must be a function. The
|
||||||
|
* context in which the listener is invoked (the value of `this`) is
|
||||||
|
* undefined. It is undefined whether LISTENER will be invoked for every
|
||||||
|
* message emitted by event EV, and such invocations may or may not be
|
||||||
|
* synchronous; it is also undefined whether LISTENER will receive
|
||||||
|
* messages emitted by event EV before having hooked the event.
|
||||||
|
*
|
||||||
|
* The validity of the event identifier EV is undefined.
|
||||||
|
*
|
||||||
|
* An implementation shall not automatically unhook LISTENER (i.e.
|
||||||
|
* without use of #removeListener) from event EV unless the
|
||||||
|
* implementation provides a message---guaranteed to be dispatched to
|
||||||
|
* LISTENER---that indicates such an action has been taken, allowing
|
||||||
|
* LISTENER to perform any necessary operations, such as cleanup or
|
||||||
|
* error handling.
|
||||||
|
*
|
||||||
|
* If a message cannot be delivered to LISTENER for any reason, the
|
||||||
|
* behavior is undefined.
|
||||||
|
*
|
||||||
|
* @param {string} ev valid event identifier
|
||||||
|
* @param {Function} listener function to invoke with message arguments
|
||||||
|
*
|
||||||
|
* @return {Eventable} self
|
||||||
|
*/
|
||||||
|
on: [ 'ev', 'listener' ],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for #on
|
||||||
|
*
|
||||||
|
* This method is provided for consistency with #removeListener and for
|
||||||
|
* compatibility with Node.js; #on is the more common (and less formal)
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @see {Eventable#on}
|
||||||
|
*/
|
||||||
|
addListener: [ 'ev', 'listener' ],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether LISTENER is listening for messages emitted by event
|
||||||
|
* EV
|
||||||
|
*
|
||||||
|
* If event EV is not valid, either `false` shall be returned or
|
||||||
|
* an error shall be thrown, since LISTENER cannot possibly hook an
|
||||||
|
* invalid event.
|
||||||
|
*
|
||||||
|
* This method is not intended as an introspective/reflection
|
||||||
|
* mechanism---it allows a consumer of an Eventable object to determine
|
||||||
|
* whether LISTENER has already been hooked before blindly
|
||||||
|
* hooking/unhooking it.
|
||||||
|
*
|
||||||
|
* @param {string} ev event id
|
||||||
|
* @param {Function} listener listener to query for
|
||||||
|
*
|
||||||
|
* @return {boolean} whether LISTENER hooks event EV
|
||||||
|
*/
|
||||||
|
hooksEvent: [ 'ev', 'listener' ],
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unhook LISTENER from event EV, preventing further messages from being
|
||||||
|
* received
|
||||||
|
*
|
||||||
|
* EV must be a valid event identifier. If LISTENER is not registered
|
||||||
|
* for event id EV, no error shall occur (since it fulfills the criteria
|
||||||
|
* that it will not be subsequently invoked by event EV).
|
||||||
|
*
|
||||||
|
* Listeners shall be compared by reference---the exact listener that
|
||||||
|
* was registered with EV must be passed for removal.
|
||||||
|
*
|
||||||
|
* This method must immediately assume that LISTENER is no longer
|
||||||
|
* capable of receiving messages and must prevent all further
|
||||||
|
* message dispatches to LISTENER; if doing so would cause errors in the
|
||||||
|
* system, then unhooking LISTENER should indicate an error condition;
|
||||||
|
* it may further request that LISTENER be re-attached, but it must not
|
||||||
|
* prevent LISTENER from being unhooked. This ensures that the system
|
||||||
|
* manging listeners can always free resources without blocking or
|
||||||
|
* entering a deadlock, and that messages intended for LISTENER can be
|
||||||
|
* dispatched elsewhere if need be (e.g. in a message queue).
|
||||||
|
*
|
||||||
|
* If a message dispatch requires confirmation of delivery (e.g. via a
|
||||||
|
* callback/continuation), and that message has already been received by
|
||||||
|
* LISTENER and is awaiting confirmation, it is undefined whether an
|
||||||
|
* implementation will await subsequent confirmation (allowing LISTENER
|
||||||
|
* to periodically register an unregister itself to throttle messages),
|
||||||
|
* or will immediately consider the delivery to be in error.
|
||||||
|
*
|
||||||
|
* @param {string} ev defined event id
|
||||||
|
* @param {Function} listener listener to remove
|
||||||
|
*
|
||||||
|
* @return {Eventable} self
|
||||||
|
*/
|
||||||
|
removeListener: [ 'ev', 'listener' ],
|
||||||
|
} );
|
||||||
|
|
|
@ -20,7 +20,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Trait = require( 'easejs' ).Trait,
|
var Trait = require( 'easejs' ).Trait,
|
||||||
isArray = require( '../std/Array' ).isArray;
|
isArray = require( '../std/Array' ).isArray,
|
||||||
|
|
||||||
|
Eventable = require( './Eventable' );
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,7 +30,9 @@ var Trait = require( 'easejs' ).Trait,
|
||||||
*
|
*
|
||||||
* The class mixing in this trait will be endowed with a complete event
|
* The class mixing in this trait will be endowed with a complete event
|
||||||
* system with which events may be registered, hooked, and emitted. There is
|
* system with which events may be registered, hooked, and emitted. There is
|
||||||
* a 1-N relationship between an event and its listeners respectively.
|
* a 1-N relationship between an event and its listeners respectively, but
|
||||||
|
* that does not prohibit an implementation from passing the same message to
|
||||||
|
* multiple events.
|
||||||
*
|
*
|
||||||
* Various aspects of this system may be overridden to provide behaviors
|
* Various aspects of this system may be overridden to provide behaviors
|
||||||
* appropriate for a variety of systems; for example, the callback scheduler
|
* appropriate for a variety of systems; for example, the callback scheduler
|
||||||
|
@ -47,7 +51,9 @@ var Trait = require( 'easejs' ).Trait,
|
||||||
* exception when emitted; and
|
* exception when emitted; and
|
||||||
* - It is implemented as a trait.
|
* - It is implemented as a trait.
|
||||||
*/
|
*/
|
||||||
module.exports = Trait( 'Evented',
|
module.exports = Trait( 'Evented' )
|
||||||
|
.implement( Eventable )
|
||||||
|
.extend(
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Registered events and their listeners
|
* Registered events and their listeners
|
||||||
|
@ -198,7 +204,7 @@ module.exports = Trait( 'Evented',
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke listener when an event is emitted
|
* Invoke LISTENER when event EV is emitted
|
||||||
*
|
*
|
||||||
* The event EV must be defined and LISTENER must be a function. The
|
* The event EV must be defined and LISTENER must be a function. The
|
||||||
* context in which the listener is invoked (the value of `this') is
|
* context in which the listener is invoked (the value of `this') is
|
||||||
|
@ -276,6 +282,9 @@ module.exports = Trait( 'Evented',
|
||||||
/**
|
/**
|
||||||
* Determine whether LISTENER is actively monitoring event EV
|
* Determine whether LISTENER is actively monitoring event EV
|
||||||
*
|
*
|
||||||
|
* If event EV does not exist, the value `false` will be returned and no
|
||||||
|
* error will be thrown.
|
||||||
|
*
|
||||||
* Note that this method does not verify by reference that the given
|
* Note that this method does not verify by reference that the given
|
||||||
* listener is legitimate---it assumes that the event metadata attached
|
* listener is legitimate---it assumes that the event metadata attached
|
||||||
* to LISTENER (if any) has not been tampered with or forged.
|
* to LISTENER (if any) has not been tampered with or forged.
|
||||||
|
@ -308,7 +317,7 @@ module.exports = Trait( 'Evented',
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a previously hooked listener in constant time, preventing it
|
* Remove a previously hooked listener in constant time, preventing it
|
||||||
* from being invoked on future emits of event EV
|
* from being invoked on future emits of event EV
|
||||||
*
|
*
|
||||||
* EV must be a valid event identifier. If LISTENER is not registered
|
* EV must be a valid event identifier. If LISTENER is not registered
|
||||||
|
|
|
@ -20,10 +20,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var Sut = require( '../../' ).event.Evented,
|
var Sut = require( '../../' ).event.Evented,
|
||||||
|
Eventable = require( '../../' ).event.Eventable,
|
||||||
|
|
||||||
Class = require( 'easejs' ).Class,
|
Class = require( 'easejs' ).Class,
|
||||||
expect = require( 'chai' ).expect,
|
expect = require( 'chai' ).expect,
|
||||||
common = require( '../lib' ),
|
common = require( '../lib' ),
|
||||||
|
|
||||||
|
|
||||||
EvStub = Class.use( Sut ).extend(
|
EvStub = Class.use( Sut ).extend(
|
||||||
{
|
{
|
||||||
// defineEvents is protected
|
// defineEvents is protected
|
||||||
|
@ -50,6 +53,21 @@ describe( 'event.Evented', () =>
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As a general-purpose library, we would not want to restrict
|
||||||
|
* developers to a specific event implementation (for example, maybe the
|
||||||
|
* user would prefer to use Node.js' event system, or transparently
|
||||||
|
* integrate with libraries/systems that use it). Together with ease.js'
|
||||||
|
* interop support, this makes such a case trivial.
|
||||||
|
*/
|
||||||
|
describe( 'interface', () =>
|
||||||
|
{
|
||||||
|
it( 'is Eventable', () =>
|
||||||
|
expect( Class.isA( Eventable, stub ) ).to.be.true
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
describe( '#defineEvents', () =>
|
describe( '#defineEvents', () =>
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue