From 7fa2a296d2a09ad60617fd3514d1042ebfbf5bfc Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Wed, 6 Aug 2014 00:03:38 -0400 Subject: [PATCH] Eventable interface with extensive specification --- src/event/Eventable.js | 165 ++++++++++++++++++++++++++++++++++++++ src/event/Evented.js | 19 +++-- test/event/EventedTest.js | 20 ++++- 3 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 src/event/Eventable.js 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', () => { /**