diff --git a/src/event/Eventable.js b/src/event/Eventable.js
new file mode 100644
index 0000000..e002ec0
--- /dev/null
+++ b/src/event/Eventable.js
@@ -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 .
+ */
+
+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' ],
+} );
+
diff --git a/src/event/Evented.js b/src/event/Evented.js
index 51ec871..9ff96b5 100644
--- a/src/event/Evented.js
+++ b/src/event/Evented.js
@@ -20,7 +20,9 @@
*/
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
* 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
* appropriate for a variety of systems; for example, the callback scheduler
@@ -47,7 +51,9 @@ var Trait = require( 'easejs' ).Trait,
* exception when emitted; and
* - It is implemented as a trait.
*/
-module.exports = Trait( 'Evented',
+module.exports = Trait( 'Evented' )
+ .implement( Eventable )
+ .extend(
{
/**
* 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
* 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
*
+ * 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
* listener is legitimate---it assumes that the event metadata attached
* 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
*
* EV must be a valid event identifier. If LISTENER is not registered
diff --git a/test/event/EventedTest.js b/test/event/EventedTest.js
index f51b729..34be455 100644
--- a/test/event/EventedTest.js
+++ b/test/event/EventedTest.js
@@ -19,11 +19,14 @@
* along with this program. If not, see .
*/
-var Sut = require( '../../' ).event.Evented,
+var Sut = require( '../../' ).event.Evented,
+ Eventable = require( '../../' ).event.Eventable,
+
Class = require( 'easejs' ).Class,
expect = require( 'chai' ).expect,
common = require( '../lib' ),
+
EvStub = Class.use( Sut ).extend(
{
// 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', () =>
{
/**