Eventable interface with extensive specification

events
Mike Gerwitz 2014-08-06 00:03:38 -04:00
parent bac61c3f08
commit 7fa2a296d2
3 changed files with 198 additions and 6 deletions

View File

@ -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' ],
} );

View File

@ -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

View File

@ -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', () =>
{ {
/** /**