├── src ├── test │ └── as │ │ ├── react │ │ ├── toString.as │ │ ├── TestError.as │ │ ├── Counter.as │ │ ├── Assert.as │ │ ├── ReactTest.as │ │ ├── ExecutorTest.as │ │ ├── ValueTest.as │ │ ├── SignalTest.as │ │ └── FutureTest.as │ │ ├── .project │ │ └── .actionScriptProperties └── main │ └── as │ └── react │ ├── IntView.as │ ├── TryView.as │ ├── BoolView.as │ ├── ObjectView.as │ ├── UintView.as │ ├── NumberView.as │ ├── StringView.as │ ├── Registration.as │ ├── UnitSignal.as │ ├── getClassName.as │ ├── FilteredSignal.as │ ├── Signal.as │ ├── Connection.as │ ├── Registrations.as │ ├── AbstractSignal.as │ ├── IntValue.as │ ├── TryValue.as │ ├── ObjectValue.as │ ├── UintValue.as │ ├── StringValue.as │ ├── BoolValue.as │ ├── SignalView.as │ ├── RegistrationGroup.as │ ├── NumberValue.as │ ├── RListener.as │ ├── MultiFailureError.as │ ├── MappedSignal.as │ ├── Promise.as │ ├── ValueView.as │ ├── Cons.as │ ├── Functions.as │ ├── Executor.as │ ├── Try.as │ ├── AbstractValue.as │ ├── Reactor.as │ ├── MappedValue.as │ ├── JoinValue.as │ └── Future.as ├── .flexLibProperties ├── pom.xml ├── .project ├── README.md ├── etc └── bootstrap.xml ├── LICENSE └── .actionScriptProperties /src/test/as/react/toString.as: -------------------------------------------------------------------------------- 1 | // 2 | // react-test 3 | 4 | package react { 5 | 6 | public function toString (val :Object) :String { 7 | return "" + val; 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/as/react/IntView.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public interface IntView extends ValueView 7 | { 8 | function get value () :int; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/as/react/TryView.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public interface TryView extends ValueView 7 | { 8 | function get value () :Try; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/as/react/BoolView.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public interface BoolView extends ValueView 7 | { 8 | function get value () :Boolean; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/as/react/ObjectView.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public interface ObjectView extends ValueView 7 | { 8 | function get value () :*; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/as/react/UintView.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public interface UintView extends ValueView 7 | { 8 | function get value () :uint; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/as/react/NumberView.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public interface NumberView extends ValueView 7 | { 8 | function get value () :Number; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/as/react/StringView.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public interface StringView extends ValueView 7 | { 8 | function get value () :String; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/as/react/Registration.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public interface Registration 7 | { 8 | /** Closes this resource stream, freeing all resources associated with it. */ 9 | function close () :void; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.flexLibProperties: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/as/react/UnitSignal.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | /** 7 | * A signal that emits an event with no associated data. 8 | */ 9 | public class UnitSignal extends AbstractSignal 10 | { 11 | /** 12 | * Causes this signal to emit an event to its connected slots. 13 | */ 14 | public function emit () :void { 15 | notifyEmit(null); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.timconkling 6 | react-as3 7 | swc 8 | 1.2.3 9 | 10 | -------------------------------------------------------------------------------- /src/test/as/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | react-test 4 | 5 | 6 | 7 | 8 | 9 | com.adobe.flexbuilder.project.flexbuilder 10 | 11 | 12 | 13 | 14 | 15 | com.adobe.flexbuilder.project.actionscriptnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/test/as/react/TestError.as: -------------------------------------------------------------------------------- 1 | // 2 | // aciv 3 | 4 | package react { 5 | 6 | public class TestError extends Error { 7 | public function TestError (message :String) { 8 | super(message); 9 | } 10 | 11 | public function equals (other :Object) :Boolean { 12 | var o :TestError = (other as TestError); 13 | return (o != null && o.message == this.message); 14 | } 15 | 16 | public function toString () :String { 17 | return "TestError: '" + this.message + "'"; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/as/react/getClassName.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | import flash.utils.getQualifiedClassName; 7 | 8 | /** 9 | * Get the full class name, e.g. "com.threerings.util.ClassUtil". 10 | * Calling getClassName with a Class object will return the same value as calling it with an 11 | * instance of that class. That is, getClassName(Foo) == getClassName(new Foo()). 12 | */ 13 | internal function getClassName (obj :Object) :String { 14 | return getQualifiedClassName(obj).replace("::", "."); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/as/react/Counter.as: -------------------------------------------------------------------------------- 1 | // 2 | // react-test 3 | 4 | package react { 5 | 6 | public class Counter { 7 | public function get slot () :Function { 8 | return onEmit; 9 | } 10 | 11 | public function trigger () :void { 12 | _count++; 13 | } 14 | 15 | public function assertTriggered (count :int, message :String = "") :void { 16 | Assert.equals(count, _count, message); 17 | } 18 | 19 | public function reset () :void { 20 | _count = 0; 21 | } 22 | 23 | public function onEmit (value :Object) :void { 24 | trigger(); 25 | } 26 | 27 | protected var _count :int; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/as/react/FilteredSignal.as: -------------------------------------------------------------------------------- 1 | // 2 | // react-as3 3 | 4 | package react { 5 | 6 | public class FilteredSignal extends MappedSignal { 7 | public function FilteredSignal (source :SignalView, pred :Function) { 8 | _source = source; 9 | _pred = pred; 10 | } 11 | 12 | override protected function connectToSource () :Connection { 13 | return _source.connect(onSourceEmit); 14 | } 15 | 16 | protected function onSourceEmit (value :Object) :void { 17 | if (_pred(value)) { 18 | notifyEmit(value); 19 | } 20 | } 21 | 22 | protected var _source :SignalView; 23 | protected var _pred :Function; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/as/react/Signal.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | /** 7 | * A signal that emits Objects. Callback functions may be connected to a signal to be 8 | * notified upon event emission. 9 | */ 10 | public class Signal extends AbstractSignal 11 | { 12 | /** 13 | * Constructs a new Signal. 14 | * The eventType parameter exists purely for documentation purposes. 15 | */ 16 | public function Signal (eventType :Class) { 17 | // no-op 18 | } 19 | 20 | /** 21 | * Causes this signal to emit the supplied event to connected slots. 22 | */ 23 | public function emit (event :Object) :void { 24 | notifyEmit(event); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | react 4 | 5 | 6 | 7 | 8 | 9 | com.adobe.flexbuilder.project.flexbuilder 10 | 11 | 12 | 13 | 14 | com.powerflasher.fdt.core.FlashBuilder 15 | 16 | 17 | 18 | 19 | 20 | com.adobe.flexbuilder.project.flexlibnature 21 | com.adobe.flexbuilder.project.actionscriptnature 22 | com.powerflasher.fdt.core.FlashNature 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/as/react/Connection.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | /** 7 | * Provides a mechanism to cancel a slot or listener registration, or to perform post-registration 8 | * adjustment like making the registration single-shot. 9 | */ 10 | public interface Connection extends Registration 11 | { 12 | /** 13 | * Converts this connection into a one-shot connection. After the first time the slot or 14 | * listener is notified, it will automatically be disconnected. 15 | * @return this connection instance for convenient chaining. 16 | */ 17 | function once () :Connection; 18 | 19 | /** 20 | * Changes the priority of this connection to the specified value. This should generally be 21 | * done simultaneously with creating a connection. 22 | * @return this connection instance for convenient chaining. 23 | */ 24 | function atPriority (priority :int) :Connection; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/as/react/Registrations.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public class Registrations 7 | { 8 | /** Returns a Registration that will call the given function when disconnected */ 9 | public static function createWithFunction (f :Function) :Registration { 10 | return new FunctionRegistration(f); 11 | } 12 | 13 | /** Returns a Registration that does nothing. */ 14 | public static function Null () :Registration { 15 | if (_null == null) { 16 | _null = new NullRegistration(); 17 | } 18 | return _null; 19 | } 20 | 21 | protected static var _null :NullRegistration; 22 | } 23 | } 24 | 25 | import react.Registration; 26 | 27 | class NullRegistration implements Registration { 28 | public function close () :void {} 29 | } 30 | 31 | class FunctionRegistration implements Registration { 32 | public function FunctionRegistration (f :Function) { 33 | _f = f; 34 | } 35 | 36 | public function close () :void { 37 | if (_f != null) { 38 | var f :Function = _f; 39 | _f = null; 40 | f(); 41 | } 42 | } 43 | 44 | protected var _f :Function; 45 | } 46 | -------------------------------------------------------------------------------- /src/main/as/react/AbstractSignal.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | /** 7 | * Handles the machinery of connecting slots to a signal and emitting events to them, without 8 | * exposing a public interface for emitting events. This can be used by entities which wish to 9 | * expose a signal-like interface for listening, without allowing external callers to emit signals. 10 | */ 11 | public class AbstractSignal extends Reactor 12 | implements SignalView 13 | { 14 | public function map (func :Function) :SignalView { 15 | return MappedSignal.create(this, func); 16 | } 17 | 18 | public function filter (pred :Function) :SignalView { 19 | return new FilteredSignal(this, pred); 20 | } 21 | 22 | public function connect (slot :Function) :Connection { 23 | return addConnection(slot); 24 | } 25 | 26 | public function disconnect (slot :Function) :void { 27 | removeConnection(slot); 28 | } 29 | 30 | /** 31 | * Emits the supplied event to all connected slots. 32 | */ 33 | protected function notifyEmit (event :Object) :void { 34 | notify(EMIT, event, null, null); 35 | } 36 | 37 | protected static const EMIT :Function = function (slot :RListener, event :Object, _1 :Object, _2 :Object) :void { 38 | slot.onEmit(event); 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/as/react/IntValue.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public class IntValue extends AbstractValue 7 | implements IntView 8 | { 9 | /** 10 | * Creates an instance with the supplied starting value. 11 | */ 12 | public function IntValue (value :int = 0) { 13 | _value = value; 14 | } 15 | 16 | public function get value () :int { 17 | return _value; 18 | } 19 | 20 | /** 21 | * Updates this instance with the supplied value. Registered listeners are notified only if the 22 | * value differs from the current value. 23 | * @return the previous value contained by this instance. 24 | */ 25 | public function set value (value :int) :void { 26 | updateAndNotifyIf(value); 27 | } 28 | 29 | /** 30 | * Updates this instance with the supplied value. Registered listeners are notified regardless 31 | * of whether the new value is equal to the old value. 32 | * @return the previous value contained by this instance. 33 | */ 34 | public function updateForce (value :int) :int { 35 | return updateAndNotify(value) as int; 36 | } 37 | 38 | override public function get () :* { 39 | return _value; 40 | } 41 | 42 | override protected function updateLocal (value :Object) :Object { 43 | var oldValue :int = _value; 44 | _value = value as int; 45 | return oldValue; 46 | } 47 | 48 | protected var _value :int; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/as/react/TryValue.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public class TryValue extends AbstractValue 7 | implements TryView 8 | { 9 | /** 10 | * Creates an instance with the supplied starting value. 11 | */ 12 | public function TryValue (value :Try = null) { 13 | _value = value; 14 | } 15 | 16 | public function get value () :Try { 17 | return _value; 18 | } 19 | 20 | /** 21 | * Updates this instance with the supplied value. Registered listeners are notified only if the 22 | * value differs from the current value. 23 | * @return the previous value contained by this instance. 24 | */ 25 | public function set value (value :Try) :void { 26 | updateAndNotifyIf(value); 27 | } 28 | 29 | /** 30 | * Updates this instance with the supplied value. Registered listeners are notified regardless 31 | * of whether the new value is equal to the old value. 32 | * @return the previous value contained by this instance. 33 | */ 34 | public function updateForce (value :Try) :Try { 35 | return Try(updateAndNotify(value)); 36 | } 37 | 38 | override public function get () :* { 39 | return _value; 40 | } 41 | 42 | override protected function updateLocal (value :Object) :Object { 43 | var oldValue :Try = _value; 44 | _value = Try(value); 45 | return oldValue; 46 | } 47 | 48 | protected var _value :Try; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/as/react/ObjectValue.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public class ObjectValue extends AbstractValue 7 | implements ObjectView 8 | { 9 | /** 10 | * Creates an instance with the supplied starting value. 11 | */ 12 | public function ObjectValue (value :Object = null) { 13 | _value = value; 14 | } 15 | 16 | public function get value () :* { 17 | return _value; 18 | } 19 | 20 | /** 21 | * Updates this instance with the supplied value. Registered listeners are notified only if the 22 | * value differs from the current value. 23 | * @return the previous value contained by this instance. 24 | */ 25 | public function set value (value :Object) :void { 26 | updateAndNotifyIf(value); 27 | } 28 | 29 | /** 30 | * Updates this instance with the supplied value. Registered listeners are notified regardless 31 | * of whether the new value is equal to the old value. 32 | * @return the previous value contained by this instance. 33 | */ 34 | public function updateForce (value :Object) :* { 35 | return updateAndNotify(value); 36 | } 37 | 38 | override public function get () :* { 39 | return _value; 40 | } 41 | 42 | override protected function updateLocal (value :Object) :Object { 43 | var oldValue :* = _value; 44 | _value = value; 45 | return oldValue; 46 | } 47 | 48 | protected var _value :Object; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/as/react/UintValue.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public class UintValue extends AbstractValue 7 | implements UintView 8 | { 9 | /** 10 | * Creates an instance with the supplied starting value. 11 | */ 12 | public function UintValue (value :uint = 0) { 13 | _value = value; 14 | } 15 | 16 | public function get value () :uint { 17 | return _value; 18 | } 19 | 20 | /** 21 | * Updates this instance with the supplied value. Registered listeners are notified only if the 22 | * value differs from the current value. 23 | * @return the previous value contained by this instance. 24 | */ 25 | public function set value (value :uint) :void { 26 | updateAndNotifyIf(value); 27 | } 28 | 29 | /** 30 | * Updates this instance with the supplied value. Registered listeners are notified regardless 31 | * of whether the new value is equal to the old value. 32 | * @return the previous value contained by this instance. 33 | */ 34 | public function updateForce (value :uint) :uint { 35 | return updateAndNotify(value) as uint; 36 | } 37 | 38 | override public function get () :* { 39 | return _value; 40 | } 41 | 42 | override protected function updateLocal (value :Object) :Object { 43 | var oldValue :uint = _value; 44 | _value = value as uint; 45 | return oldValue; 46 | } 47 | 48 | protected var _value :uint; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/as/react/StringValue.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public class StringValue extends AbstractValue 7 | implements StringView 8 | { 9 | /** 10 | * Creates an instance with the supplied starting value. 11 | */ 12 | public function StringValue (value :String = null) { 13 | _value = value; 14 | } 15 | 16 | public function get value () :String { 17 | return _value; 18 | } 19 | 20 | /** 21 | * Updates this instance with the supplied value. Registered listeners are notified only if the 22 | * value differs from the current value. 23 | * @return the previous value contained by this instance. 24 | */ 25 | public function set value (value :String) :void { 26 | updateAndNotifyIf(value); 27 | } 28 | 29 | /** 30 | * Updates this instance with the supplied value. Registered listeners are notified regardless 31 | * of whether the new value is equal to the old value. 32 | * @return the previous value contained by this instance. 33 | */ 34 | public function updateForce (value :String) :* { 35 | return updateAndNotify(value); 36 | } 37 | 38 | override public function get () :* { 39 | return _value; 40 | } 41 | 42 | override protected function updateLocal (value :Object) :Object { 43 | var oldValue :String = _value; 44 | _value = value as String; 45 | return oldValue; 46 | } 47 | 48 | protected var _value :String; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/as/react/BoolValue.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public class BoolValue extends AbstractValue 7 | implements BoolView 8 | { 9 | /** 10 | * Creates an instance with the supplied starting value. 11 | */ 12 | public function BoolValue (value :Boolean = false) { 13 | _value = value; 14 | } 15 | 16 | public function get value () :Boolean { 17 | return _value; 18 | } 19 | 20 | /** 21 | * Updates this instance with the supplied value. Registered listeners are notified only if the 22 | * value differs from the current value. 23 | * @return the previous value contained by this instance. 24 | */ 25 | public function set value (value :Boolean) :void { 26 | updateAndNotifyIf(value); 27 | } 28 | 29 | /** 30 | * Updates this instance with the supplied value. Registered listeners are notified regardless 31 | * of whether the new value is equal to the old value. 32 | * @return the previous value contained by this instance. 33 | */ 34 | public function updateForce (value :Boolean) :Boolean { 35 | return updateAndNotify(value) as Boolean; 36 | } 37 | 38 | override public function get () :* { 39 | return _value; 40 | } 41 | 42 | override protected function updateLocal (value :Object) :Object { 43 | var oldValue :Boolean = _value; 44 | _value = value as Boolean; 45 | return oldValue; 46 | } 47 | 48 | protected var _value :Boolean; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/as/react/SignalView.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | /** 7 | * A view of a {@link Signal}, on which slots may listen, but to which one cannot emit events. This 8 | * is generally used to provide signal-like views of changing entities. See {@link AbstractValue} 9 | * for an example. 10 | */ 11 | public interface SignalView 12 | { 13 | /** 14 | * Creates a signal that maps this signal via a function. When this signal emits a value, the 15 | * mapped signal will emit that value as transformed by the supplied function. The mapped value 16 | * will retain a connection to this signal for as long as it has connections of its own. 17 | */ 18 | function map (func :Function) :SignalView; 19 | 20 | /** 21 | * Creates a signal that emits a value only when the supplied filter function returns true. The 22 | * filtered signal will retain a connection to this signal for as long as it has connections of 23 | * its own. 24 | */ 25 | function filter (pred :Function) :SignalView; 26 | 27 | /** 28 | * Connects this signal to the supplied function, such that when an event is emitted from this 29 | * signal, the function will be called. 30 | * 31 | * @return a connection instance which can be used to cancel the connection. 32 | */ 33 | function connect (slot :Function) :Connection; 34 | 35 | /** 36 | * Disconnects the supplied function from this signal if connect was called with it. 37 | * If the slot has been connected multiple times, all connections are cancelled. 38 | */ 39 | function disconnect (slot :Function) :void; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/as/react/RegistrationGroup.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | import flash.utils.Dictionary; 7 | 8 | /** 9 | * Collects Registrations to allow mass operations on them. 10 | */ 11 | public class RegistrationGroup 12 | implements Registration 13 | { 14 | /** 15 | * Adds a Registration to the manager. 16 | * @return the Registration passed to the function. 17 | */ 18 | public function add (r :Registration) :Registration { 19 | if (_regs == null) { 20 | _regs = new Dictionary(); 21 | } 22 | _regs[r] = true; 23 | return r; 24 | } 25 | 26 | /** Removes a Registration from the group without disconnecting it. */ 27 | public function remove (r :Registration) :void { 28 | if (_regs != null) { 29 | delete _regs[r]; 30 | } 31 | } 32 | 33 | /** Closes all Registrations that have been added to the manager. */ 34 | public function close () :void { 35 | if (_regs != null) { 36 | var regs :Dictionary = _regs; 37 | _regs = null; 38 | 39 | var err :MultiFailureError = null; 40 | for (var r :Registration in regs) { 41 | try { 42 | r.close(); 43 | } catch (e :Error) { 44 | if (err == null) { 45 | err = new MultiFailureError(); 46 | } 47 | err.addFailure(e); 48 | } 49 | } 50 | 51 | if (err != null) { 52 | throw err; 53 | } 54 | } 55 | } 56 | 57 | private var _regs :Dictionary; // lazily instantiated 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/main/as/react/NumberValue.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | public class NumberValue extends AbstractValue 7 | implements NumberView 8 | { 9 | /** 10 | * Creates an instance with the supplied starting value. 11 | */ 12 | public function NumberValue (value :Number = 0) { 13 | _value = value; 14 | } 15 | 16 | public function get value () :Number { 17 | return _value; 18 | } 19 | 20 | /** 21 | * Updates this instance with the supplied value. Registered listeners are notified only if the 22 | * value differs from the current value. 23 | * @return the previous value contained by this instance. 24 | */ 25 | public function set value (value :Number) :void { 26 | updateAndNotifyIf(value); 27 | } 28 | 29 | /** 30 | * Updates this instance with the supplied value. Registered listeners are notified regardless 31 | * of whether the new value is equal to the old value. 32 | * @return the previous value contained by this instance. 33 | */ 34 | public function updateForce (value :Number) :Number { 35 | return updateAndNotify(value) as Number; 36 | } 37 | 38 | override public function get () :* { 39 | return _value; 40 | } 41 | 42 | override protected function updateLocal (value :Object) :Object { 43 | var oldValue :Number = _value; 44 | _value = value as Number; 45 | return oldValue; 46 | } 47 | 48 | override protected function valuesAreEqual (value1 :Object, value2 :Object) :Boolean { 49 | return value1 == value2 || (isNaN(Number(value1)) && isNaN(Number(value2))); 50 | } 51 | 52 | protected var _value :Number; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React-as3 2 | ========= 3 | 4 | React is a low-level library that provides [signal/slot] and [functional 5 | reactive programming]-like primitives. It can serve as the basis for a user 6 | interface toolkit, or any other library that has a model on which clients will 7 | listen and to which they will react. 8 | 9 | React-as3 is a port of [React](http://github.com/threerings/react), a Java 10 | library created by Three Rings Design. 11 | 12 | Building 13 | -------- 14 | 15 | The library is built using [Ant] (or with and IDE like [Flash Builder]). 16 | 17 | From the command line, invoke `ant` to build the library, or `ant maven-deploy` 18 | to build and deploy it to your local Maven repository. 19 | 20 | There's also a Flash Builder project at the root level of the repository. 21 | 22 | Artifacts 23 | --------- 24 | 25 | To add a React-as3 dependency to a Maven project, add the following to your 26 | `pom.xml`: 27 | 28 | 29 | 30 | com.timconkling 31 | react-as3 32 | 1.2.1 33 | 34 | 35 | 36 | Distribution 37 | ------------ 38 | 39 | React-as3 is released under the New BSD License. The most recent version of the 40 | library is available at http://github.com/tconkling/react-as3 41 | 42 | Contact 43 | ------- 44 | 45 | Feel free to open issues on the project's Github home. 46 | 47 | Twitter: [@timconkling](http://twitter.com/timconkling) 48 | 49 | [signal/slot]: http://en.wikipedia.org/wiki/Signals_and_slots 50 | [functional reactive programming]: http://en.wikipedia.org/wiki/Functional_reactive_programming 51 | [Maven]: http://maven.apache.org/ 52 | [Ant]: http://ant.apache.org/ 53 | [Flash Builder]: http://www.adobe.com/products/flash-builder.html 54 | -------------------------------------------------------------------------------- /src/main/as/react/RListener.as: -------------------------------------------------------------------------------- 1 | // 2 | // React 3 | 4 | package react { 5 | 6 | import flash.errors.IllegalOperationError; 7 | 8 | public /*abstract*/ class RListener 9 | { 10 | public static function create (f :Function) :RListener { 11 | switch (f.length) { 12 | case 2: return new RListener2(f); 13 | case 1: return new RListener1(f); 14 | default: return new RListener0(f); 15 | } 16 | } 17 | 18 | public function onEmit (val :Object) :void { 19 | throw new IllegalOperationError("abstract"); 20 | } 21 | 22 | public function onChange (val1 :Object, val2 :Object) :void { 23 | throw new IllegalOperationError("abstract"); 24 | } 25 | 26 | public function RListener (f :Function) { 27 | _f = f; 28 | } 29 | 30 | internal function get f () :Function { 31 | return _f; 32 | } 33 | 34 | protected var _f :Function; 35 | } 36 | 37 | } 38 | 39 | import react.RListener; 40 | 41 | class RListener0 extends RListener { 42 | public function RListener0 (f :Function) { 43 | super(f); 44 | } 45 | 46 | override public function onEmit (val :Object) :void { 47 | _f(); 48 | } 49 | 50 | override public function onChange (val1 :Object, val2 :Object) :void { 51 | _f(); 52 | } 53 | } 54 | 55 | class RListener1 extends RListener { 56 | public function RListener1 (f :Function) { 57 | super(f); 58 | } 59 | 60 | override public function onEmit (val :Object) :void { 61 | _f(val); 62 | } 63 | 64 | override public function onChange (val1 :Object, val2 :Object) :void { 65 | _f(val1); 66 | } 67 | } 68 | 69 | class RListener2 extends RListener { 70 | public function RListener2 (f :Function) { 71 | super(f); 72 | } 73 | 74 | override public function onEmit (val :Object) :void { 75 | _f(val, undefined); 76 | } 77 | 78 | override public function onChange (val1 :Object, val2 :Object) :void { 79 | _f(val1, val2); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /etc/bootstrap.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 18 | 19 | 20 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/as/react/MultiFailureError.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | import flash.events.ErrorEvent; 7 | import flash.events.UncaughtErrorEvent; 8 | 9 | /** 10 | * An exception thrown to communicate multiple listener failures. 11 | */ 12 | public class MultiFailureError extends Error 13 | { 14 | public function get failures () :Vector. { 15 | return _failures; 16 | } 17 | 18 | public function addFailure (e :Object) :void { 19 | if (e is MultiFailureError) { 20 | _failures = _failures.concat(MultiFailureError(e).failures); 21 | } else { 22 | _failures[_failures.length] = e; 23 | } 24 | this.message = getMessage(); 25 | } 26 | 27 | public function getMessage () :String { 28 | var buf :String = ""; 29 | for each (var failure :Object in _failures) { 30 | if (buf.length > 0) { 31 | buf += ", "; 32 | } 33 | buf += getMessageInternal(failure, false); 34 | } 35 | return "" + _failures.length + (_failures.length != 1 ? " failures: " : " failure: ") + buf; 36 | } 37 | 38 | private static function getMessageInternal (error :*, wantStackTrace :Boolean) :String { 39 | // NB: do NOT use the class-cast operator for converting to typed error objects. 40 | // Error() is a top-level function that creates a new error object, rather than performing 41 | // a class-cast, as expected. 42 | 43 | if (error is Error) { 44 | var e :Error = (error as Error); 45 | return (wantStackTrace ? e.getStackTrace() : e.message || ""); 46 | } else if (error is UncaughtErrorEvent) { 47 | return getMessageInternal(error.error, wantStackTrace); 48 | } else if (error is ErrorEvent) { 49 | var ee :ErrorEvent = (error as ErrorEvent); 50 | return getClassName(ee) + 51 | " [errorID=" + ee.errorID + 52 | ", type='" + ee.type + "'" + 53 | ", text='" + ee.text + "']"; 54 | } 55 | 56 | return "An error occurred: " + error; 57 | } 58 | 59 | private var _failures :Vector. = new Vector.(); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/as/react/MappedSignal.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | import flash.errors.IllegalOperationError; 7 | 8 | /** 9 | * Plumbing to implement mapped signals in such a way that they automatically manage a connection 10 | * to their underlying signal. When the mapped signal adds its first connection, it establishes a 11 | * connection to the underlying signal, and when it removes its last connection it clears its 12 | * connection from the underlying signal. 13 | */ 14 | public /*abstract*/ class MappedSignal extends AbstractSignal 15 | { 16 | public static function create (source :SignalView, f :Function) :MappedSignal { 17 | return new MappedSignalImpl(source, f); 18 | } 19 | 20 | /** 21 | * Establishes a connection to our source signal. Called when we go from zero to one listeners. 22 | * When we go from one to zero listeners, the connection will automatically be cleared. 23 | * 24 | * @return the newly established connection. 25 | */ 26 | protected /*abstract*/ function connectToSource () :Connection { 27 | throw new IllegalOperationError("abstract"); 28 | } 29 | 30 | override protected function connectionAdded () :void { 31 | super.connectionAdded(); 32 | if (_conn == null) { 33 | _conn = connectToSource(); 34 | } 35 | } 36 | 37 | override protected function connectionRemoved () :void { 38 | super.connectionRemoved(); 39 | if (!this.hasConnections && _conn != null) { 40 | _conn.close(); 41 | _conn = null; 42 | } 43 | } 44 | 45 | protected var _conn :Connection; 46 | } 47 | } 48 | 49 | import react.Connection; 50 | import react.MappedSignal; 51 | import react.SignalView; 52 | 53 | class MappedSignalImpl extends MappedSignal { 54 | public function MappedSignalImpl (source :SignalView, f :Function) { 55 | _source = source; 56 | _f = f; 57 | } 58 | 59 | override protected function connectToSource () :Connection { 60 | return _source.connect(onSourceEmit); 61 | } 62 | 63 | protected function onSourceEmit (value :Object) :void { 64 | notifyEmit(_f(value)); 65 | } 66 | 67 | protected var _source :SignalView; 68 | protected var _f :Function; 69 | } 70 | -------------------------------------------------------------------------------- /src/main/as/react/Promise.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | /** 7 | * Provides a concrete implementation {@link Future} that can be updated with a success or failure 8 | * result when it becomes available. 9 | * 10 | *

This implementation also guarantees a useful behavior, which is that all listeners added 11 | * prior to the completion of the promise will be cleared when the promise is completed, and no 12 | * further listeners will be retained. This allows the promise to be retained after is has been 13 | * completed as a useful "box" for its underlying value, without concern that references to long 14 | * satisfied listeners will be inadvertently retained.

15 | */ 16 | public class Promise extends Future 17 | { 18 | /** Creates a new, uncompleted, promise. */ 19 | public function Promise () { 20 | super(_value = new PromiseValue()); 21 | } 22 | 23 | /** Causes this promise to be completed successfully with {@code value}. */ 24 | public function succeed (value :Object = null) :void { 25 | _value.value = Try.success(value); 26 | } 27 | 28 | /** 29 | * Causes this promise to be completed with failure caused by {@code cause}. 30 | * 'cause' can be an Error, ErrorEvent, or String, and will be converted to an Error. 31 | */ 32 | public function fail (cause :Object) :void { 33 | _value.value = Try.failure(cause); 34 | } 35 | 36 | /** Returns a function that can be used to complete this promise. */ 37 | public function get completer () :Function { 38 | return _value.slot; 39 | 40 | } 41 | 42 | /** Returns true if there are listeners awaiting the completion of this promise. */ 43 | public function get hasConnections () :Boolean { 44 | return _value.hasConnections; 45 | } 46 | 47 | protected var _value :PromiseValue; 48 | } 49 | 50 | } 51 | 52 | import react.TryValue; 53 | 54 | class PromiseValue extends TryValue { 55 | override protected function updateAndNotify (value :Object, force :Boolean = true) :Object { 56 | if (_value != null) { 57 | throw new Error("Already completed"); 58 | } 59 | try { 60 | return super.updateAndNotify(value, force); 61 | } finally { 62 | _listeners = null; // clear out our listeners now that they have been notified 63 | } 64 | return null; // compiler too dumb to realize we'll never get here 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/as/react/Assert.as: -------------------------------------------------------------------------------- 1 | // 2 | // aciv 3 | 4 | package react { 5 | 6 | public class Assert { 7 | public static function isTrue (condition :Boolean, failureMessage :String = "") :void { 8 | if (!condition) { 9 | throw new Error(failureMessage); 10 | } 11 | } 12 | 13 | public static function isFalse (condition :Boolean, failureMessage :String = "") :void { 14 | isTrue(!condition, failureMessage); 15 | } 16 | 17 | public static function equals (a :*, b :*, failureMessage :String = null) :void { 18 | if (!testEquals(a, b)) { 19 | if (failureMessage == null) { 20 | try { 21 | failureMessage = "" + a + " != " + b; 22 | } catch (e :Error) { 23 | failureMessage = ""; 24 | } 25 | } 26 | throw new Error(failureMessage); 27 | } 28 | } 29 | 30 | public static function epsilonEquals (a :Number, b :Number, epsilon :Number, failureMessage :String="") :void { 31 | if (Math.abs(b - a) > epsilon) { 32 | throw new Error(failureMessage); 33 | } 34 | } 35 | 36 | public static function throws (f :Function, errorClass :Class = null, failureMessage :String="") :void { 37 | try { 38 | f(); 39 | } catch (e :Error) { 40 | if (errorClass != null && !(e is errorClass)) { 41 | throw new Error(failureMessage); 42 | } 43 | return; 44 | } 45 | throw new Error(failureMessage); 46 | } 47 | 48 | private static function testEquals (a :*, b :*) :Boolean { 49 | if (a === b) { 50 | return true; 51 | } else if (a is TestError) { 52 | return TestError(a).equals(b); 53 | } else { 54 | var aVec :Vector. = (a as Vector.); 55 | var bVec :Vector. = (b as Vector.); 56 | if (aVec != null && bVec != null) { 57 | if (aVec.length != bVec.length) { 58 | return false; 59 | } 60 | 61 | for (var ii :int = 0; ii < aVec.length; ++ii) { 62 | if (!testEquals(aVec[ii], bVec[ii])) { 63 | return false; 64 | } 65 | } 66 | 67 | return true; 68 | } 69 | 70 | return false; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/as/react/ReactTest.as: -------------------------------------------------------------------------------- 1 | // 2 | // react-test 3 | 4 | package react { 5 | 6 | import flash.display.Sprite; 7 | 8 | public class ReactTest extends Sprite 9 | { 10 | public function ReactTest() { 11 | registrationGroupTest(); 12 | signalTest(); 13 | valueTest(); 14 | futureTest(); 15 | executorTest(); 16 | 17 | trace("all tests passed"); 18 | } 19 | 20 | private function registrationGroupTest () :void { 21 | var group :RegistrationGroup = new RegistrationGroup(); 22 | var sig :UnitSignal = new UnitSignal(); 23 | var counter :Counter = new Counter(); 24 | group.add(sig.connect(counter.slot)); 25 | sig.emit(); 26 | group.close(); 27 | sig.emit(); 28 | 29 | counter.assertTriggered(1, "RegistrationGroup close all connections"); 30 | } 31 | 32 | private function signalTest () :void { 33 | var suite :SignalTest = new SignalTest(); 34 | suite.testSignalToSlot(); 35 | suite.testOneShotSlot(); 36 | suite.testSlotPriority(); 37 | suite.testAddDuringDispatch(); 38 | suite.testRemoveDuringDispatch(); 39 | suite.testAddAndRemoveDuringDispatch(); 40 | suite.testUnitSlot(); 41 | suite.testSingleFailure(); 42 | suite.testMultiFailure(); 43 | suite.testMappedSignal(); 44 | } 45 | 46 | private function valueTest () :void { 47 | var suite :ValueTest = new ValueTest(); 48 | suite.testSimpleListener(); 49 | suite.testAsSignal(); 50 | suite.testAsOnceSignal(); 51 | suite.testMappedValue(); 52 | suite.testConnectNotify(); 53 | suite.testDisconnect(); 54 | suite.testSlot(); 55 | } 56 | 57 | private function futureTest () :void { 58 | var suite :FutureTest = new FutureTest(); 59 | suite.testImmediate(); 60 | suite.testDeferred(); 61 | suite.testMappedImmediate(); 62 | suite.testMappedDeferred(); 63 | suite.testFlatMapValues(); 64 | suite.testFlatMappedImmediate(); 65 | suite.testFlatMappedDeferred(); 66 | suite.testFlatMappedDoubleDeferred(); 67 | suite.testSequenceImmediate(); 68 | suite.testSequenceDeferred(); 69 | suite.testCollectEmpty(); 70 | suite.testCollectImmediate(); 71 | suite.testCollectDeferred(); 72 | } 73 | 74 | private function executorTest () :void { 75 | var suite :ExecutorTest = new ExecutorTest(); 76 | suite.testSubmitImmediate(); 77 | suite.testSubmitFuture(); 78 | suite.testSubmitMany(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/as/react/ValueView.as: -------------------------------------------------------------------------------- 1 | // 2 | // React 3 | 4 | package react { 5 | 6 | /** 7 | * A view of a {@link AbstractValue} subclass, to which listeners may be added, but which one 8 | * cannot update. Value consumers should require only a view on a value, rather than a 9 | * concrete value. 10 | */ 11 | public interface ValueView 12 | { 13 | /** 14 | * Returns the current value. 15 | */ 16 | function get () :*; 17 | 18 | /** 19 | * Creates a value that maps this value via a function. When this value changes, the mapped 20 | * listeners will be notified, regardless of whether the new and old mapped values differ. The 21 | * mapped value will retain a connection to this value for as long as it has connections of its 22 | * own. 23 | */ 24 | function map (func :Function) :ValueView; 25 | 26 | /** 27 | * Creates a BoolView that maps this value via a function. 28 | */ 29 | function mapToBool (func :Function) :BoolView; 30 | 31 | /** 32 | * Creates an IntView that maps this value via a function. 33 | */ 34 | function mapToInt (func :Function) :IntView; 35 | 36 | /** 37 | * Creates a UintView that maps this value via a function. 38 | */ 39 | function mapToUint (func :Function) :UintView; 40 | 41 | /** 42 | * Creates a NumberView that maps this value via a function. 43 | */ 44 | function mapToNumber (func :Function) :NumberView; 45 | 46 | /** 47 | * Creates a TryView that maps this value via a function. 48 | */ 49 | function mapToTry (func :Function) :TryView; 50 | 51 | /** 52 | * Connects the supplied Function to this value, such that it will be notified when this value 53 | * changes. 54 | * @return a connection instance which can be used to cancel the connection. 55 | */ 56 | function connect (listener :Function) :Connection; 57 | 58 | /** 59 | * Connects the supplied listener to this value, such that it will be notified when this value 60 | * changes. Also immediately notifies the listener of the current value. Note that the previous 61 | * value supplied with this notification will be null. If the notification triggers an 62 | * unchecked exception, the slot will automatically be disconnected and the caller need not 63 | * worry about cleaning up after itself. 64 | * @return a connection instance which can be used to cancel the connection. 65 | */ 66 | function connectNotify (listener :Function) :Connection; 67 | 68 | /** 69 | * Disconnects the supplied listener from this value if it's connected. If the listener has been 70 | * connected multiple times, all connections are cancelled. 71 | */ 72 | function disconnect (listener :Function) :void; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/as/react/Cons.as: -------------------------------------------------------------------------------- 1 | // 2 | // React 3 | 4 | package react { 5 | 6 | /** 7 | * Implements {@link Connection} and a linked-list style listener list for {@link Reactor}s. 8 | */ 9 | internal class Cons implements Connection 10 | { 11 | /** The next connection in our chain. */ 12 | public var next :Cons; 13 | 14 | public function Cons (owner :Reactor, listener :RListener) { 15 | _owner = owner; 16 | _listener = listener; 17 | } 18 | 19 | /** Indicates whether this connection is one-shot or persistent. */ 20 | public final function oneShot () :Boolean { 21 | return _oneShot; 22 | } 23 | 24 | /** Returns the listener for this cons cell. */ 25 | public function get listener () :RListener { 26 | return _listener; 27 | } 28 | 29 | public function close () :void { 30 | // multiple disconnects are OK, we just NOOP after the first one 31 | if (_owner != null) { 32 | _owner.removeCons(this); 33 | _owner = null; 34 | _listener = null; 35 | } 36 | } 37 | 38 | public function once () :Connection { 39 | _oneShot = true; 40 | return this; 41 | } 42 | 43 | public function atPriority (priority :int) :Connection { 44 | if (_owner == null) { 45 | throw new Error("Cannot change priority of disconnected connection."); 46 | } 47 | _owner.removeCons(this); 48 | next = null; 49 | _priority = priority; 50 | _owner.addCons(this); 51 | return this; 52 | } 53 | 54 | internal static function insert (head :Cons, cons :Cons) :Cons { 55 | if (head == null) { 56 | return cons; 57 | } else if (cons._priority > head._priority) { 58 | cons.next = head; 59 | return cons; 60 | } else { 61 | head.next = insert(head.next, cons); 62 | return head; 63 | } 64 | } 65 | 66 | internal static function remove (head :Cons, cons :Cons) :Cons { 67 | if (head == null) { 68 | return head; 69 | } else if (head == cons) { 70 | return head.next; 71 | } else { 72 | head.next = remove(head.next, cons); 73 | return head; 74 | } 75 | } 76 | 77 | internal static function removeAll (head :Cons, listener :Function) :Cons { 78 | if (head == null) { 79 | return null; 80 | } else if (head.listener.f == listener) { 81 | return removeAll(head.next, listener); 82 | } else { 83 | head.next = removeAll(head.next, listener); 84 | return head; 85 | } 86 | } 87 | 88 | private var _owner :Reactor; 89 | private var _listener :RListener; 90 | private var _oneShot :Boolean; 91 | private var _priority :int; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/as/react/Functions.as: -------------------------------------------------------------------------------- 1 | // 2 | // React 3 | 4 | package react { 5 | import flash.utils.Dictionary; 6 | 7 | /** 8 | * Various Function-related utility methods. 9 | */ 10 | public class Functions 11 | { 12 | /** The identity function */ 13 | public static function IDENT (value :Object) :Object { 14 | return value; 15 | } 16 | 17 | /** Implements boolean not. */ 18 | public static function NOT (value :Boolean) :Boolean { 19 | return !value; 20 | } 21 | 22 | /** A function that applies {@link String#valueOf} to its argument. */ 23 | public static function TO_STRING (value :Object) :String { 24 | return value.toString(); 25 | } 26 | 27 | /** A function that returns true for null values and false for non-null values. */ 28 | public static function IS_NULL (value :Object) :Boolean { 29 | return (value == null); 30 | } 31 | 32 | /** A function that returns true for non-null values and false for null values. */ 33 | public static function NON_NULL (value :Object) :Boolean { 34 | return (value != null); 35 | } 36 | 37 | /** 38 | * Returns a function that always returns the supplied constant value. 39 | */ 40 | public static function constant (constant :Object) :Function { 41 | return function (value :Object) :Object { 42 | return constant; 43 | }; 44 | } 45 | 46 | /** 47 | * Returns a function that computes whether a value is greater than {@code target}. 48 | */ 49 | public static function greaterThan (target :int) :Function { 50 | return function (value :int) :Boolean { 51 | return value > target; 52 | }; 53 | } 54 | 55 | /** 56 | * Returns a function that computes whether a value is greater than or equal to {@code value}. 57 | */ 58 | public static function greaterThanEqual (target :int) :Function { 59 | return function (value :int) :Boolean { 60 | return value >= target; 61 | }; 62 | } 63 | 64 | /** 65 | * Returns a function that computes whether a value is less than {@code target}. 66 | */ 67 | public static function lessThan (target :int) :Function { 68 | return function (value :int) :Boolean { 69 | return value < target; 70 | }; 71 | } 72 | 73 | /** 74 | * Returns a function that computes whether a value is less than or equal to {@code target}. 75 | */ 76 | public static function lessThanEqual (target :int) :Function { 77 | return function (value :int) :Boolean { 78 | return value <= target; 79 | }; 80 | } 81 | 82 | /** 83 | * Returns a function which performs a Dictionary lookup with a default value. The function created by 84 | * this method returns defaultValue for all inputs that do not belong to the dict's key set. 85 | */ 86 | public static function forDict (dict :Dictionary, defaultValue :Object) :Function { 87 | return function (key :Object) :Object { 88 | return (key in dict ? dict[key] : defaultValue); 89 | }; 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | react-as3 - a library for functional-reactive-like programming in ActionScript 2 | Copyright (c) 2013, Tim Conkling 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | * The name Tim Conkling may not be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | Ported from: 28 | 29 | React - a library for functional-reactive-like programming in Java 30 | Copyright (c) 2011, Three Rings Design, Inc. 31 | All rights reserved. 32 | 33 | Redistribution and use in source and binary forms, with or without 34 | modification, are permitted provided that the following conditions are met: 35 | 36 | * Redistributions of source code must retain the above copyright notice, 37 | this list of conditions and the following disclaimer. 38 | * Redistributions in binary form must reproduce the above copyright notice, 39 | this list of conditions and the following disclaimer in the documentation 40 | and/or other materials provided with the distribution. 41 | * The name Three Rings may not be used to endorse or promote products 42 | derived from this software without specific prior written permission. 43 | 44 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 45 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 46 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 47 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 48 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 49 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 50 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 51 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 52 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 53 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 54 | 55 | -------------------------------------------------------------------------------- /src/main/as/react/Executor.as: -------------------------------------------------------------------------------- 1 | // 2 | // react-as3 3 | 4 | package react { 5 | 6 | /** 7 | * An object that executes "tasks" which take time to complete. Allows controlling the number of 8 | * concurrently-executing tasks. 9 | */ 10 | public class Executor { 11 | /** 12 | * Number of tasks that can run concurrently on this Executor. 13 | * If maxSimultaneous <= 0, there is no concurrency limit. 14 | */ 15 | public var maxSimultaneous :int; 16 | 17 | public function Executor (maxSimultaneous :int = 0) { 18 | this.maxSimultaneous = maxSimultaneous; 19 | } 20 | 21 | /** Number of tasks currently running on the Exector. */ 22 | public function get numRunning () :uint { 23 | return _numRunning; 24 | } 25 | 26 | /** Number of tasks currently pending on the Executor. */ 27 | public function get numPending () :uint { 28 | return _pending.length; 29 | } 30 | 31 | /** True if the Executor will immediately run a new task passed to it. */ 32 | public function get hasCapacity () :Boolean { 33 | return this.maxSimultaneous <= 0 || _numRunning < this.maxSimultaneous; 34 | } 35 | 36 | /** 37 | * Submit a Function to the Executor. The Function will be run on the Executor at 38 | * some point in the future. If the submitted Function returns a Future, that Future's 39 | * result will be flat-mapped onto the returned Future. Otherwise, the returned Future 40 | * will succeed with the output of the function, or fail with any Error thrown by the Function. 41 | */ 42 | public function submit (f :Function) :Future { 43 | var task :ExecutorTask = new ExecutorTask(f); 44 | _pending.unshift(task); 45 | runNextIfAvailable(); 46 | return task.promise; 47 | } 48 | 49 | private function runNextIfAvailable () :void { 50 | if (_pending.length > 0 && this.hasCapacity) { 51 | runTask(_pending.pop()); 52 | } 53 | } 54 | 55 | private function runTask (task :ExecutorTask) :void { 56 | _numRunning++; 57 | var val :*; 58 | try { 59 | val = task.func(); 60 | } catch (e :Error) { 61 | task.promise.fail(e); 62 | _numRunning--; 63 | runNextIfAvailable(); 64 | return; 65 | } 66 | 67 | var futureVal :Future = (val as Future); 68 | if (futureVal != null) { 69 | futureVal.onComplete(function (result :Try) :void { 70 | if (result.isSuccess) { 71 | task.promise.succeed(result.value); 72 | } else { 73 | task.promise.fail(result.failure); 74 | } 75 | _numRunning--; 76 | runNextIfAvailable(); 77 | }); 78 | 79 | } else { 80 | task.promise.succeed(val); 81 | _numRunning--; 82 | runNextIfAvailable(); 83 | } 84 | } 85 | 86 | private var _numRunning :uint; 87 | private var _pending :Vector. = new []; 88 | } 89 | } 90 | 91 | import react.Promise; 92 | 93 | class ExecutorTask { 94 | public const promise :Promise = new Promise(); 95 | public var func :Function; 96 | 97 | public function ExecutorTask (func :Function) { 98 | this.func = func; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /.actionScriptProperties: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/test/as/.actionScriptProperties: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/test/as/react/ExecutorTest.as: -------------------------------------------------------------------------------- 1 | // 2 | // react-as3 3 | 4 | package react { 5 | 6 | public class ExecutorTest { 7 | public function testSubmitImmediate () :void { 8 | var exec :Executor = new Executor(1); 9 | assertExecState(exec, 0, 0, true); 10 | 11 | var successValue :* = null; 12 | exec.submit(function () :String { 13 | return "Yay"; 14 | }).onSuccess(function (value :*) :void { 15 | successValue = value; 16 | }); 17 | 18 | assertExecState(exec, 0, 0, true); 19 | Assert.equals(successValue, "Yay"); 20 | 21 | var failureValue :* = null; 22 | exec.submit(function () :String { 23 | throw new TestError("Boo!"); 24 | }).onFailure(function (err :*) :void { 25 | failureValue = err; 26 | }); 27 | 28 | assertExecState(exec, 0, 0, true); 29 | Assert.equals(failureValue, new TestError("Boo!")); 30 | } 31 | 32 | public function testSubmitFuture () :void { 33 | var exec :Executor = new Executor(1); 34 | 35 | var successValue :* = null; 36 | var successPromise :Promise = new Promise(); 37 | exec.submit(function () :Future { 38 | return successPromise; 39 | }).onSuccess(function (value :*) :void { 40 | successValue = value; 41 | }); 42 | 43 | assertExecState(exec, 1, 0, false); 44 | Assert.equals(successValue, null); 45 | 46 | successPromise.succeed("Yay"); 47 | assertExecState(exec, 0, 0, true); 48 | Assert.equals(successValue, "Yay"); 49 | 50 | var failValue :* = null; 51 | var failPromise :Promise = new Promise(); 52 | exec.submit(function () :Future { 53 | return failPromise; 54 | }).onFailure(function (value :*) :void { 55 | failValue = value; 56 | }); 57 | 58 | assertExecState(exec, 1, 0, false); 59 | Assert.equals(failValue, null); 60 | 61 | failPromise.fail("Boo!"); 62 | assertExecState(exec, 0, 0, true); 63 | Assert.equals(failValue, "Boo!"); 64 | } 65 | 66 | public function testSubmitMany () :void { 67 | var exec :Executor = new Executor(2); 68 | var p1 :Promise = new Promise(); 69 | var p2 :Promise = new Promise(); 70 | var p3 :Promise = new Promise(); 71 | var p4 :Promise = new Promise(); 72 | 73 | exec.submit(wrap(p1)); 74 | exec.submit(wrap(p2)); 75 | 76 | var val3 :* = null; 77 | exec.submit(wrap(p3)).onSuccess(function (value :*) :void { 78 | val3 = value; 79 | }); 80 | 81 | var val4 :* = null; 82 | exec.submit(wrap(p4)).onSuccess(function (value :*) :void { 83 | val4 = value; 84 | }); 85 | 86 | assertExecState(exec, 2, 2, false); 87 | Assert.equals(val3, null); 88 | Assert.equals(val4, null); 89 | 90 | p4.succeed("Yay4!"); 91 | 92 | // p1, p2 running; p3, p4 pending 93 | assertExecState(exec, 2, 2, false); 94 | Assert.equals(val3, null); 95 | Assert.equals(val4, null); 96 | 97 | // p1, p3 running; p2 complete; p4 pending 98 | p2.succeed("Yay2!"); 99 | 100 | assertExecState(exec, 2, 1, false); 101 | Assert.equals(val3, null); 102 | Assert.equals(val4, null); 103 | 104 | // p1 running; p2, p3, p4 complete; nobody pending 105 | p3.succeed("Yay3!"); 106 | 107 | assertExecState(exec, 1, 0, true); 108 | Assert.equals(val3, "Yay3!"); 109 | Assert.equals(val4, "Yay4!"); 110 | 111 | p1.succeed("Yay1!"); 112 | 113 | // Everyone is complete 114 | assertExecState(exec, 0, 0, true); 115 | } 116 | 117 | private static function wrap (f :Future) :Function { 118 | return function () :Future { 119 | return f; 120 | }; 121 | } 122 | 123 | private static function assertExecState (exec :Executor, numRunning :uint, numPending :uint, hasCapacity :Boolean) :void { 124 | Assert.equals(exec.numRunning, numRunning); 125 | Assert.equals(exec.numPending, numPending); 126 | Assert.equals(exec.hasCapacity, hasCapacity); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/test/as/react/ValueTest.as: -------------------------------------------------------------------------------- 1 | // 2 | // React 3 | 4 | package react { 5 | 6 | /** 7 | * Tests aspects of the {@link Value} class. 8 | */ 9 | public class ValueTest 10 | { 11 | public function testSimpleListener () :void { 12 | var value :IntValue = new IntValue(42); 13 | var fired :Boolean = false; 14 | value.connect(function (nvalue :int, ovalue :int) :void { 15 | Assert.equals(42, ovalue); 16 | Assert.equals(15, nvalue); 17 | fired = true; 18 | }); 19 | 20 | Assert.equals(42, value.updateForce(15)); 21 | Assert.equals(15, value.get()); 22 | Assert.isTrue(fired); 23 | } 24 | 25 | public function testAsSignal () :void { 26 | var value :IntValue = new IntValue(42); 27 | var fired :Boolean = false; 28 | value.connect(function (value :int) :void { 29 | Assert.equals(15, value); 30 | fired = true; 31 | }); 32 | value.value = 15; 33 | Assert.isTrue(fired); 34 | } 35 | 36 | public function testAsOnceSignal () :void { 37 | var value :IntValue = new IntValue(42); 38 | var counter :Counter = new Counter(); 39 | value.connect(counter.onEmit).once(); 40 | value.value = 15; 41 | value.value = 42; 42 | counter.assertTriggered(1); 43 | } 44 | 45 | public function testMappedValue () :void { 46 | var value :IntValue = new IntValue(42); 47 | var mapped :ValueView = value.map(toString); 48 | 49 | var counter :Counter = new Counter(); 50 | var c1 :Connection = mapped.connect(counter.onEmit); 51 | var c2 :Connection = mapped.connect(SignalTest.require("15")); 52 | 53 | value.value = 15; 54 | counter.assertTriggered(1); 55 | value.value = 15; 56 | counter.assertTriggered(1); 57 | value.updateForce(15); 58 | counter.assertTriggered(2); 59 | 60 | // disconnect from the mapped value and ensure that it disconnects in turn 61 | c1.close(); 62 | c2.close(); 63 | Assert.isTrue(!value.hasConnections); 64 | } 65 | 66 | public function testConnectNotify () :void { 67 | var value :IntValue = new IntValue(42); 68 | var fired :Boolean = false; 69 | value.connectNotify(function (val :int) :void { 70 | Assert.equals(42, val); 71 | fired = true; 72 | }); 73 | Assert.isTrue(fired); 74 | } 75 | 76 | public function testDisconnect () :void { 77 | var value :IntValue = new IntValue(42); 78 | var expectedValue :int = value.get(); 79 | var fired :int = 0; 80 | 81 | var listener :Function = function (newValue :int) :void { 82 | Assert.equals(expectedValue, newValue); 83 | fired += 1; 84 | value.disconnect(listener); 85 | }; 86 | 87 | var conn :Connection = value.connectNotify(listener); 88 | value.value = expectedValue = 12; 89 | Assert.equals(1, fired, "Disconnecting in listenNotify disconnects"); 90 | conn.close();// Just see what happens when calling disconnect while disconnected 91 | 92 | value.connect(listener); 93 | value.connect(new Counter().onEmit); 94 | value.connect(listener); 95 | value.value = expectedValue = 13; 96 | value.value = expectedValue = 14; 97 | Assert.equals(3, fired, "Disconnecting in listen disconnects"); 98 | 99 | value.connect(listener).close(); 100 | value.value = expectedValue = 15; 101 | Assert.equals(3, fired, "Disconnecting before geting an update still disconnects"); 102 | } 103 | 104 | public function testSlot () :void { 105 | var value :IntValue = new IntValue(42); 106 | var expectedValue :int = value.get(); 107 | var fired :int = 0; 108 | var listener :Function = function (newValue :int) :void { 109 | Assert.equals(expectedValue, newValue); 110 | fired += 1; 111 | value.disconnect(listener); 112 | }; 113 | value.connect(listener); 114 | value.value = expectedValue = 12; 115 | Assert.equals(1, fired, "Calling disconnect with a slot disconnects"); 116 | 117 | value.connect(listener).close(); 118 | value.value = expectedValue = 14; 119 | Assert.equals(1, fired); 120 | } 121 | } 122 | 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/main/as/react/Try.as: -------------------------------------------------------------------------------- 1 | // 2 | // React 3 | 4 | package react { 5 | 6 | import flash.errors.IllegalOperationError; 7 | 8 | /** 9 | * Represents a computation that either provided a result, or failed with an exception. Monadic 10 | * methods are provided that allow one to map and compose tries in ways that propagate failure. 11 | * This class is not itself "reactive", but it facilitates a more straightforward interface and 12 | * implementation for {@link Future} and {@link Promise}. 13 | */ 14 | public /*abstract*/ class Try 15 | { 16 | /** Creates a successful try. */ 17 | public static function success (value :Object) :Try { return new Success(value); } 18 | 19 | /** 20 | * Creates a failed try. 21 | * 'cause' can be an Error, ErrorEvent, or String, and will be converted to an Error. 22 | */ 23 | public static function failure (cause :Object) :Try { return new Failure(cause); } 24 | 25 | /** Lifts {@code func}, a function on values, to a function on tries. */ 26 | public static function lift (func :Function) :Function { 27 | return function (result :Try) :Object { 28 | return result.map(func); 29 | }; 30 | } 31 | 32 | /** Returns the value associated with a successful try, or rethrows the exception if the try 33 | * failed. */ 34 | public function get value () :* { return abstract(); } 35 | 36 | /** Returns the cause of failure for a failed try. Throws {@link IllegalOperationError} if 37 | * called on a successful try. */ 38 | public /*abstract*/ function get failure () :* { return abstract(); } 39 | 40 | /** Returns true if this is a successful try, false if it is a failed try. */ 41 | public /*abstract*/ function get isSuccess () :Boolean { return abstract(); } 42 | 43 | /** Returns true if this is a failed try, false if it is a successful try. */ 44 | public final function get isFailure () :Boolean { return !this.isSuccess; } 45 | 46 | /** Maps successful tries through {@code func}, passees failure through as is. */ 47 | public /*abstract*/ function map (func :Function) :Try { return abstract(); } 48 | 49 | /** Maps successful tries through {@code func}, passes failure through as is. */ 50 | public /*abstract*/ function flatMap (func :Function) :Try { return abstract(); } 51 | 52 | private static function abstract () :* { 53 | throw new IllegalOperationError("abstract"); 54 | } 55 | } 56 | 57 | } 58 | 59 | import flash.errors.IllegalOperationError; 60 | 61 | import react.Try; 62 | 63 | /** Represents a successful try. Contains the successful result. */ 64 | class Success extends Try { 65 | public function Success (value :Object) { 66 | _value = value; 67 | } 68 | 69 | override public function get value () :* { 70 | return _value; 71 | } 72 | 73 | override public function get failure () :* { 74 | throw new IllegalOperationError(); 75 | } 76 | 77 | override public function get isSuccess () :Boolean { 78 | return true; 79 | } 80 | 81 | override public function map (func :Function) :Try { 82 | try { 83 | return Try.success(func(_value)); 84 | } catch (e :Error) { 85 | return Try.failure(e); 86 | } 87 | } 88 | 89 | override public function flatMap (func :Function) :Try { 90 | return func(_value); 91 | } 92 | 93 | public function toString () :String { 94 | return "Success(" + value + ")"; 95 | } 96 | 97 | protected var _value :Object; 98 | } 99 | 100 | /** Represents a failed try. Contains the cause of failure. */ 101 | class Failure extends Try { 102 | public function Failure (cause :Object) { 103 | _cause = cause; 104 | } 105 | 106 | override public function get value () :* { 107 | throw new IllegalOperationError(); 108 | } 109 | 110 | override public function get failure () :* { 111 | return _cause; 112 | } 113 | 114 | override public function get isSuccess () :Boolean { 115 | return false; 116 | } 117 | 118 | override public function map (func :Function) :Try { 119 | return this; 120 | } 121 | 122 | override public function flatMap (func :Function) :Try { 123 | return this; 124 | } 125 | 126 | public function toString () :String { 127 | return "Failure(" + value + ")"; 128 | } 129 | 130 | protected var _cause :Object; 131 | } 132 | -------------------------------------------------------------------------------- /src/main/as/react/AbstractValue.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | import flash.errors.IllegalOperationError; 7 | 8 | /** 9 | * Handles the machinery of connecting listeners to a value and notifying them, without exposing a 10 | * public interface for updating the value. This can be used by libraries which wish to provide 11 | * observable values, but must manage the maintenance and distribution of value updates themselves 12 | * (so that they may send them over the network, for example). 13 | */ 14 | public /*abstract*/ class AbstractValue extends Reactor 15 | implements ValueView 16 | { 17 | public /*abstract*/ function get () :* { 18 | throw new IllegalOperationError("abstract"); 19 | } 20 | 21 | /** Returns a "slot" Function which simply calls through to the Value's setter function. */ 22 | public function get slot () :Function { 23 | return this.updateAndNotifyIf; 24 | } 25 | 26 | public function map (func :Function) :ValueView { 27 | return MappedValue.create(this, func); 28 | } 29 | 30 | public function mapToBool (func :Function) :BoolView { 31 | return MappedValue.boolView(this, func); 32 | } 33 | 34 | public function mapToInt (func :Function) :IntView { 35 | return MappedValue.intView(this, func); 36 | } 37 | 38 | public function mapToUint (func :Function) :UintView { 39 | return MappedValue.uintView(this, func); 40 | } 41 | 42 | public function mapToNumber (func :Function) :NumberView { 43 | return MappedValue.numberView(this, func); 44 | } 45 | 46 | public function mapToTry (func :Function) :TryView { 47 | return MappedValue.tryView(this, func); 48 | } 49 | 50 | public function connect (listener :Function) :Connection { 51 | return addConnection(listener); 52 | } 53 | 54 | public function connectNotify (listener :Function) :Connection { 55 | // connect before calling emit; if the listener changes the value in the body of onEmit, it 56 | // will expect to be notified of that change; however if onEmit throws a runtime exception, 57 | // we need to take care of disconnecting the listener because the returned connection 58 | // instance will never reach the caller 59 | var cons :Cons = addConnection(listener); 60 | try { 61 | cons.listener.onChange(get(), null); 62 | } catch (e :Error) { 63 | cons.close(); 64 | throw e; 65 | } 66 | return cons; 67 | } 68 | 69 | public function disconnect (listener :Function) :void { 70 | removeConnection(listener); 71 | } 72 | 73 | public function toString () :String { 74 | var cname :String = getClassName(this); 75 | return cname.substring(cname.lastIndexOf(".")+1) + "(" + get() + ")"; 76 | } 77 | 78 | /** 79 | * Updates the value contained in this instance and notifies registered listeners iff said 80 | * value is not equal to the value already contained in this instance. 81 | */ 82 | protected function updateAndNotifyIf (value :Object) :Object { 83 | return updateAndNotify(value, false); 84 | } 85 | 86 | /** 87 | * Updates the value contained in this instance and notifies registered listeners. 88 | * @return the previously contained value. 89 | */ 90 | protected function updateAndNotify (value :Object, force :Boolean = true) :Object { 91 | checkMutate(); 92 | var ovalue :Object = updateLocal(value); 93 | if (force || !valuesAreEqual(value, ovalue)) { 94 | emitChange(value, ovalue); 95 | } 96 | return ovalue; 97 | } 98 | 99 | /** 100 | * Emits a change notification. Default implementation immediately notifies listeners. 101 | */ 102 | protected function emitChange (value :Object, oldValue :Object) :void { 103 | notifyChange(value, oldValue); 104 | } 105 | 106 | /** 107 | * Notifies our listeners of a value change. 108 | */ 109 | protected function notifyChange (value :Object, oldValue :Object) :void { 110 | notify(CHANGE, value, oldValue, null); 111 | } 112 | 113 | /** 114 | * Updates our locally stored value. Default implementation throws IllegalOperationError. 115 | * @return the previously stored value. 116 | */ 117 | protected function updateLocal (value :Object) :Object { 118 | throw new IllegalOperationError(); 119 | } 120 | 121 | /** 122 | * Override to customize the comparison done in updateAndNotify to decide if an update will 123 | * be performed if a force is not requested. 124 | */ 125 | protected function valuesAreEqual (value1 :Object, value2 :Object) :Boolean { 126 | return value1 == value2; 127 | } 128 | 129 | protected static function CHANGE (l :RListener, value :Object, oldValue :Object, _ :Object) :void { 130 | l.onChange(value, oldValue); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/as/react/Reactor.as: -------------------------------------------------------------------------------- 1 | // 2 | // React 3 | 4 | package react { 5 | 6 | /** 7 | * A base class for all reactive classes. This is an implementation detail, but is public so that 8 | * third parties may use it to create their own reactive classes, if desired. 9 | */ 10 | public /*abstract*/ class Reactor 11 | { 12 | /** 13 | * Returns true if this reactor has at least one connection. 14 | */ 15 | public function get hasConnections () :Boolean { 16 | return _listeners != null; 17 | } 18 | 19 | protected function addConnection (listener :Function) :Cons { 20 | if (listener == null) { 21 | throw new ArgumentError("Null listener"); 22 | } 23 | return addCons(new Cons(this, RListener.create(listener))); 24 | } 25 | 26 | protected function removeConnection (listener :Function) :void { 27 | if (this.isDispatching) { 28 | _pendingRuns = insert(_pendingRuns, new Runs(function () :void { 29 | _listeners = Cons.removeAll(_listeners, listener); 30 | connectionRemoved(); 31 | })); 32 | } else { 33 | _listeners = Cons.removeAll(_listeners, listener); 34 | connectionRemoved(); 35 | } 36 | } 37 | 38 | /** 39 | * Emits the supplied event to all connected slots. 40 | */ 41 | protected function notify (notifier :Function, a1 :Object, a2 :Object, a3 :Object) :void { 42 | if (_listeners == null) { 43 | // Bail early if we have no listeners 44 | return; 45 | } else if (_listeners == DISPATCHING) { 46 | throw new Error("Initiated notify while notifying"); 47 | } 48 | 49 | var lners :Cons = _listeners; 50 | _listeners = DISPATCHING; 51 | 52 | var error :Error = null; 53 | try { 54 | for (var cons :Cons = lners; cons != null; cons = cons.next) { 55 | // cons.listener will be null if Cons was closed after iteration started 56 | if (cons.listener != null) { 57 | try { 58 | notifier(cons.listener, a1, a2, a3); 59 | } catch (e :Error) { 60 | error = e; 61 | } 62 | if (cons.oneShot()) { 63 | cons.close(); 64 | } 65 | } 66 | } 67 | 68 | if (error != null) { 69 | throw error; 70 | } 71 | } finally { 72 | // note that we're no longer dispatching 73 | _listeners = lners; 74 | 75 | // now remove listeners any queued for removing and add any queued for adding 76 | for (; _pendingRuns != null; _pendingRuns = _pendingRuns.next) { 77 | _pendingRuns.action(); 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * Called prior to mutating any underlying model; allows subclasses to reject mutation. 84 | */ 85 | protected function checkMutate () :void { 86 | // noop 87 | } 88 | 89 | /** 90 | * Called when a connection has been added to this reactor. 91 | */ 92 | protected function connectionAdded () :void { 93 | // noop 94 | } 95 | 96 | /** 97 | * Called when a connection may have been removed from this reactor. 98 | */ 99 | protected function connectionRemoved () :void { 100 | // noop 101 | } 102 | 103 | internal function addCons (cons :Cons) :Cons { 104 | if (this.isDispatching) { 105 | _pendingRuns = insert(_pendingRuns, new Runs(function () :void { 106 | _listeners = Cons.insert(_listeners, cons); 107 | connectionAdded(); 108 | })); 109 | } else { 110 | _listeners = Cons.insert(_listeners, cons); 111 | connectionAdded(); 112 | } 113 | return cons; 114 | } 115 | 116 | internal function removeCons (cons :Cons) :void { 117 | if (this.isDispatching) { 118 | _pendingRuns = insert(_pendingRuns, new Runs(function () :void { 119 | _listeners = Cons.remove(_listeners, cons); 120 | connectionRemoved(); 121 | })); 122 | } else { 123 | _listeners = Cons.remove(_listeners, cons); 124 | connectionRemoved(); 125 | } 126 | } 127 | 128 | private function get isDispatching () :Boolean { 129 | return _listeners == DISPATCHING; 130 | } 131 | 132 | protected static function insert (head :Runs, action :Runs) :Runs { 133 | if (head == null) { 134 | return action; 135 | } else { 136 | head.next = insert(head.next, action); 137 | return head; 138 | } 139 | } 140 | 141 | protected var _listeners :Cons; 142 | protected var _pendingRuns :Runs; 143 | 144 | protected static const DISPATCHING :Cons = new Cons(null, null); 145 | } 146 | 147 | } 148 | 149 | class Runs { 150 | public var next :Runs; 151 | public var action :Function; 152 | 153 | public function Runs (action :Function) { 154 | this.action = action; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/as/react/MappedValue.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | import flash.errors.IllegalOperationError; 7 | 8 | /** 9 | * Plumbing to implement mapped values in such a way that they automatically manage a connection to 10 | * their underlying value. When the mapped value adds its first connection, it establishes a 11 | * connection to the underlying value, and when it removes its last connection it clears its 12 | * connection from the underlying value. 13 | */ 14 | public /*abstract*/ class MappedValue extends AbstractValue 15 | { 16 | public static function create (source :ValueView, map :Function) :ValueView { 17 | return new MappedValueImpl(source, map); 18 | } 19 | 20 | public static function boolView (source :ValueView, map :Function) :BoolView { 21 | return new MappedBool(source, map); 22 | } 23 | 24 | public static function intView (source :ValueView, map :Function) :IntView { 25 | return new MappedInt(source, map); 26 | } 27 | 28 | public static function uintView (source :ValueView, map :Function) :UintView { 29 | return new MappedUint(source, map); 30 | } 31 | 32 | public static function numberView (source :ValueView, map :Function) :NumberView { 33 | return new MappedNumber(source, map); 34 | } 35 | 36 | public static function objectView (source :ValueView, map :Function) :ObjectView { 37 | return new MappedObject(source, map); 38 | } 39 | 40 | public static function tryView (source :ValueView, map :Function) :TryView { 41 | return new MappedTry(source, map); 42 | } 43 | 44 | /** 45 | * Establishes a connection to our source value. Called when we go from zero to one listeners. 46 | * When we go from one to zero listeners, the connection will automatically be cleared. 47 | * 48 | * @return the newly established connection. 49 | */ 50 | protected /*abstract*/ function connectToSource () :Connection { 51 | throw new IllegalOperationError("abstract"); 52 | } 53 | 54 | override protected function connectionAdded () :void { 55 | super.connectionAdded(); 56 | if (_conn == null) { 57 | _conn = connectToSource(); 58 | } 59 | } 60 | 61 | override protected function connectionRemoved () :void { 62 | super.connectionRemoved(); 63 | if (!this.hasConnections && _conn != null) { 64 | _conn.close(); 65 | _conn = null; 66 | } 67 | } 68 | 69 | protected var _conn :Connection; 70 | } 71 | } 72 | 73 | import react.BoolView; 74 | import react.Connection; 75 | import react.IntView; 76 | import react.MappedValue; 77 | import react.NumberView; 78 | import react.ObjectView; 79 | import react.Try; 80 | import react.TryView; 81 | import react.UintView; 82 | import react.ValueView; 83 | 84 | class MappedValueImpl extends MappedValue { 85 | public function MappedValueImpl (source :ValueView, f :Function) { 86 | _source = source; 87 | _f = f; 88 | } 89 | 90 | override public function get () :* { 91 | return _f(_source.get()); 92 | } 93 | 94 | override protected function connectToSource () :Connection { 95 | return _source.connect(onSourceChange); 96 | } 97 | 98 | protected function onSourceChange (value :Object, ovalue :Object) :void { 99 | notifyChange(_f(value), _f(ovalue)); 100 | } 101 | 102 | protected var _source :ValueView; 103 | protected var _f :Function; 104 | } 105 | 106 | class MappedBool extends MappedValueImpl implements BoolView { 107 | public function MappedBool (source :ValueView, f :Function) { 108 | super(source, f); 109 | } 110 | 111 | public function get value () :Boolean { 112 | return _f(_source.get()); 113 | } 114 | } 115 | 116 | class MappedInt extends MappedValueImpl implements IntView { 117 | public function MappedInt (source :ValueView, f :Function) { 118 | super(source, f); 119 | } 120 | 121 | public function get value () :int { 122 | return _f(_source.get()); 123 | } 124 | } 125 | 126 | class MappedUint extends MappedValueImpl implements UintView { 127 | public function MappedUint (source :ValueView, f :Function) { 128 | super(source, f); 129 | } 130 | 131 | public function get value () :uint { 132 | return _f(_source.get()); 133 | } 134 | } 135 | 136 | class MappedNumber extends MappedValueImpl implements NumberView { 137 | public function MappedNumber (source :ValueView, f :Function) { 138 | super(source, f); 139 | } 140 | 141 | public function get value () :Number { 142 | return _f(_source.get()); 143 | } 144 | } 145 | 146 | class MappedObject extends MappedValueImpl implements ObjectView { 147 | public function MappedObject (source :ValueView, f :Function) { 148 | super(source, f); 149 | } 150 | 151 | public function get value () :* { 152 | return _f(_source.get()); 153 | } 154 | } 155 | 156 | class MappedTry extends MappedValueImpl implements TryView { 157 | public function MappedTry (source :ValueView, f :Function) { 158 | super(source, f); 159 | } 160 | 161 | public function get value () :Try { 162 | return _f(_source.get()); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/as/react/JoinValue.as: -------------------------------------------------------------------------------- 1 | // 2 | // react 3 | 4 | package react { 5 | 6 | import flash.errors.IllegalOperationError; 7 | 8 | /** 9 | * Plumbing to implement "join" values -- values that are dependent on multiple underlying 10 | * source values -- in such a way that they automatically manage a connection 11 | * to their underlying values. When the JoinValue adds its first connection, it establishes a 12 | * connection to each underlying value, and when it removes its last connection it clears its 13 | * connection from each underlying value. 14 | */ 15 | public class JoinValue extends AbstractValue 16 | { 17 | /** Mapping function that computes the 'and' of its boolean sources */ 18 | public static const AND :Function = function (sources :Array) :Boolean { 19 | for each (var source :ValueView in sources) { 20 | if (!Boolean(source.get())) { 21 | return false; 22 | } 23 | } 24 | return true; 25 | }; 26 | 27 | /** Mapping function that computes the 'or' of its boolean sources */ 28 | public static const OR :Function = function (sources :Array) :Boolean { 29 | for each (var source :ValueView in sources) { 30 | if (Boolean(source.get())) { 31 | return true; 32 | } 33 | } 34 | return false; 35 | }; 36 | 37 | /** Mapping function that computes the sum of its numeric sources */ 38 | public static const SUM :Function = function (sources :Array) :Number { 39 | var sum :Number = 0; 40 | for each (var source :ValueView in sources) { 41 | sum += source.get(); 42 | } 43 | return sum; 44 | }; 45 | 46 | public static function create (sources :Array, map :Function) :ValueView { 47 | return new JoinValueImpl(sources, map); 48 | } 49 | 50 | public static function boolView (sources :Array, map :Function) :BoolView { 51 | return new JoinBool(sources, map); 52 | } 53 | 54 | public static function intView (sources :Array, map :Function) :IntView { 55 | return new JoinInt(sources, map); 56 | } 57 | 58 | public static function uintView (sources :Array, map :Function) :UintView { 59 | return new JoinUint(sources, map); 60 | } 61 | 62 | public static function numberView (sources :Array, map :Function) :NumberView { 63 | return new JoinNumber(sources, map); 64 | } 65 | 66 | public static function objectView (sources :Array, map :Function) :ObjectView { 67 | return new JoinObject(sources, map); 68 | } 69 | 70 | /** 71 | * Establishes a connection to our source value. Called when we go from zero to one listeners. 72 | * When we go from one to zero listeners, the connection will automatically be cleared. 73 | * 74 | * @return a vector of the newly established connections. 75 | */ 76 | protected /*abstract*/ function connectToSources () :Vector. { 77 | throw new IllegalOperationError("abstract"); 78 | } 79 | 80 | override protected function connectionAdded () :void { 81 | super.connectionAdded(); 82 | if (_conns == null) { 83 | _conns = connectToSources(); 84 | } 85 | } 86 | 87 | override protected function connectionRemoved () :void { 88 | super.connectionRemoved(); 89 | if (!this.hasConnections && _conns != null) { 90 | for each (var conn :Connection in _conns) { 91 | conn.close(); 92 | } 93 | _conns = null; 94 | } 95 | } 96 | 97 | protected var _conns :Vector.; 98 | } 99 | } 100 | 101 | import react.BoolView; 102 | import react.Connection; 103 | import react.IntView; 104 | import react.JoinValue; 105 | import react.NumberView; 106 | import react.ObjectView; 107 | import react.UintView; 108 | import react.ValueView; 109 | 110 | class JoinValueImpl extends JoinValue { 111 | public function JoinValueImpl (sources :Array, f :Function) { 112 | _sources = sources; 113 | _f = f; 114 | } 115 | 116 | override public function get () :* { 117 | // If we don't have connections, we need to update every time we're called, 118 | // since we're not being notified when underlying values change. 119 | return (this.hasConnections ? _curValue : update()); 120 | } 121 | 122 | protected function update () :* { 123 | return (_f.length == 0 ? _f() : _f(_sources)); 124 | } 125 | 126 | override protected function connectToSources () :Vector. { 127 | var out :Vector. = new Vector.(_sources.length, true); 128 | for (var ii :int = 0; ii < _sources.length; ++ii) { 129 | out[ii] = ValueView(_sources[ii]).connect(onSourceChange); 130 | } 131 | _curValue = update(); 132 | return out; 133 | } 134 | 135 | protected function onSourceChange (value :Object, ovalue :Object) :void { 136 | var newVal :* = update(); 137 | if (newVal != _curValue) { 138 | var oldVal :* = _curValue; 139 | _curValue = newVal; 140 | notifyChange(_curValue, oldVal); 141 | } 142 | } 143 | 144 | protected static function GET (view :ValueView, _ :int, __ :Array) :* { 145 | return view.get(); 146 | } 147 | 148 | protected var _sources :Array; 149 | protected var _curValue :* = undefined; 150 | protected var _f :Function; 151 | } 152 | 153 | class JoinBool extends JoinValueImpl implements BoolView { 154 | public function JoinBool (sources :Array, f :Function) { 155 | super(sources, f); 156 | } 157 | 158 | public function get value () :Boolean { 159 | return get(); 160 | } 161 | } 162 | 163 | class JoinInt extends JoinValueImpl implements IntView { 164 | public function JoinInt (sources :Array, f :Function) { 165 | super(sources, f); 166 | } 167 | 168 | public function get value () :int { 169 | return get(); 170 | } 171 | } 172 | 173 | class JoinUint extends JoinValueImpl implements UintView { 174 | public function JoinUint (sources :Array, f :Function) { 175 | super(sources, f); 176 | } 177 | 178 | public function get value () :uint { 179 | return get(); 180 | } 181 | } 182 | 183 | class JoinNumber extends JoinValueImpl implements NumberView { 184 | public function JoinNumber (sources :Array, f :Function) { 185 | super(sources, f); 186 | } 187 | 188 | public function get value () :Number { 189 | return get(); 190 | } 191 | } 192 | 193 | class JoinObject extends JoinValueImpl implements ObjectView { 194 | public function JoinObject (sources :Array, f :Function) { 195 | super(sources, f); 196 | } 197 | 198 | public function get value () :* { 199 | return get(); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/test/as/react/SignalTest.as: -------------------------------------------------------------------------------- 1 | // 2 | // React 3 | 4 | package react { 5 | 6 | /** 7 | * Tests basic signals and slots behavior. 8 | */ 9 | public class SignalTest 10 | { 11 | public static function require (reqValue :Object) :Function { 12 | return function (value :Object) :void { 13 | Assert.equals(reqValue, value); 14 | }; 15 | } 16 | 17 | public function testSignalToSlot () :void { 18 | var signal :Signal = new Signal(int); 19 | var slot :AccSlot = new AccSlot(); 20 | signal.connect(slot.onEmit); 21 | signal.emit(1); 22 | signal.emit(2); 23 | signal.emit(3); 24 | Assert.equals(new [1, 2, 3], slot.events); 25 | } 26 | 27 | public function testOneShotSlot () :void { 28 | var signal :Signal = new Signal(int); 29 | var slot :AccSlot = new AccSlot(); 30 | signal.connect(slot.onEmit).once(); 31 | signal.emit(1); // slot should be removed after this emit 32 | signal.emit(2); 33 | signal.emit(3); 34 | Assert.equals(new [1], slot.events); 35 | } 36 | 37 | public function testSlotPriority () :void { 38 | var counter :Object = { val: 0 }; 39 | var slot1 :PriorityTestSlot = new PriorityTestSlot(counter); 40 | var slot2 :PriorityTestSlot = new PriorityTestSlot(counter); 41 | var slot3 :PriorityTestSlot = new PriorityTestSlot(counter); 42 | var slot4 :PriorityTestSlot = new PriorityTestSlot(counter); 43 | 44 | var signal :UnitSignal = new UnitSignal(); 45 | signal.connect(slot3.onEmit).atPriority(3); 46 | signal.connect(slot1.onEmit).atPriority(1); 47 | signal.connect(slot2.onEmit).atPriority(2); 48 | signal.connect(slot4.onEmit).atPriority(4); 49 | signal.emit(); 50 | Assert.equals(4, slot1.order); 51 | Assert.equals(3, slot2.order); 52 | Assert.equals(2, slot3.order); 53 | Assert.equals(1, slot4.order); 54 | } 55 | 56 | public function testAddDuringDispatch () :void { 57 | var signal :Signal = new Signal(int); 58 | var toAdd :AccSlot = new AccSlot(); 59 | 60 | signal.connect(function () :void { 61 | signal.connect(toAdd.onEmit); 62 | }).once(); 63 | 64 | // this will connect our new signal but not dispatch to it 65 | signal.emit(5); 66 | Assert.equals(0, toAdd.events.length); 67 | 68 | // now dispatch an event that should go to the added signal 69 | signal.emit(42); 70 | Assert.equals(new [42], toAdd.events); 71 | } 72 | 73 | public function testRemoveDuringDispatch () :void { 74 | var signal :Signal = new Signal(int); 75 | var toRemove :AccSlot = new AccSlot(); 76 | var rconn :Connection = signal.connect(toRemove.onEmit); 77 | 78 | // dispatch one event and make sure it's received 79 | signal.emit(5); 80 | Assert.equals(new [5], toRemove.events); 81 | 82 | // now add our removing signal, and dispatch again 83 | signal.connect(function () :void { 84 | rconn.close(); 85 | }).atPriority(1); // ensure that we're before toRemove 86 | signal.emit(42); 87 | 88 | // toRemove will have been removed during this dispatch, so it should not have received 89 | // the signal 90 | Assert.equals(new [5], toRemove.events); 91 | } 92 | 93 | public function testAddAndRemoveDuringDispatch () :void { 94 | var signal :Signal = new Signal(int); 95 | var toAdd :AccSlot = new AccSlot(); 96 | var toRemove :AccSlot = new AccSlot(); 97 | var rconn :Connection = signal.connect(toRemove.onEmit); 98 | 99 | // dispatch one event and make sure it's received by toRemove 100 | signal.emit(5); 101 | Assert.equals(new [5], toRemove.events); 102 | 103 | // now add our adder/remover signal, and dispatch again 104 | signal.connect(function () :void { 105 | rconn.close(); 106 | signal.connect(toAdd.onEmit); 107 | }); 108 | signal.emit(42); 109 | // make sure toRemove got this event and toAdd didn't 110 | Assert.equals(new [5, 42], toRemove.events); 111 | Assert.equals(0, toAdd.events.length); 112 | 113 | // finally emit one more and ensure that toAdd got it and toRemove didn't 114 | signal.emit(9); 115 | Assert.equals(new [9], toAdd.events); 116 | Assert.equals(new [5, 42], toRemove.events); 117 | } 118 | 119 | public function testUnitSlot () :void { 120 | var signal :Signal = new Signal(int); 121 | var fired :Boolean = false; 122 | signal.connect(function () :void { 123 | fired = true; 124 | }); 125 | signal.emit(42); 126 | Assert.isTrue(fired); 127 | } 128 | 129 | public function testSingleFailure () :void { 130 | Assert.throws(function () :void { 131 | var signal :UnitSignal = new UnitSignal(); 132 | signal.connect(function () :void { 133 | throw new Error("Bang!"); 134 | }); 135 | signal.emit(); 136 | }); 137 | } 138 | 139 | public function testMultiFailure () :void { 140 | Assert.throws(function () :void { 141 | var signal :UnitSignal = new UnitSignal(); 142 | signal.connect(function () :void { 143 | throw new Error("Bing!"); 144 | }); 145 | signal.connect(function () :void { 146 | throw new Error("Bang!"); 147 | }); 148 | signal.emit(); 149 | }, Error); 150 | } 151 | 152 | public function testMappedSignal () :void { 153 | var signal :Signal = new Signal(int); 154 | var mapped :SignalView = signal.map(toString); 155 | 156 | var counter :Counter = new Counter(); 157 | var c1 :Connection = mapped.connect(counter.onEmit); 158 | var c2 :Connection = mapped.connect(SignalTest.require("15")); 159 | 160 | signal.emit(15); 161 | counter.assertTriggered(1); 162 | signal.emit(15); 163 | counter.assertTriggered(2); 164 | 165 | // disconnect from the mapped signal and ensure that it clears its connection 166 | c1.close(); 167 | c2.close(); 168 | Assert.isTrue(!signal.hasConnections); 169 | } 170 | } 171 | 172 | } 173 | 174 | class AccSlot { 175 | public var events :Vector. = new Vector.(); 176 | public function onEmit (event :Object) :void { 177 | events.push(event); 178 | } 179 | } 180 | 181 | class PriorityTestSlot { 182 | public var order :int; 183 | public var counter :Object; 184 | 185 | public function PriorityTestSlot (counter :Object) { 186 | this.counter = counter; 187 | } 188 | 189 | public function onEmit (event :Object) :void { 190 | order = ++(counter.val); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/as/react/Future.as: -------------------------------------------------------------------------------- 1 | // 2 | // React 3 | 4 | package react { 5 | 6 | /** 7 | * Represents an asynchronous result. Unlike standard Java futures, you cannot block on this 8 | * result. You can {@link #map} or {@link #flatMap} it, and listen for success or failure via the 9 | * {@link #success} and {@link #failure} signals. 10 | * 11 | *

The benefit over just using a callback is that results can be composed. You can 12 | * subscribe to an object, flatmap the result into a service call on that object which returns the 13 | * address of another object, flat map that into a request to subscribe to that object, and finally 14 | * pass the resulting object to some other code via a slot. Failure can be handled once for all of 15 | * these operations and you avoid nesting yourself three callbacks deep.

16 | */ 17 | public class Future { 18 | 19 | /** Returns a future with a pre-existing success value. */ 20 | public static function success (value :Object = null) :Future { 21 | return result(Try.success(value)); 22 | } 23 | 24 | /** 25 | * Returns a future with a pre-existing failure value. 26 | * 'cause' can be an Error, ErrorEvent, or String, and will be converted to an Error. 27 | */ 28 | public static function failure (cause :Object) :Future { 29 | return result(Try.failure(cause)); 30 | } 31 | 32 | /** Returns a future with an already-computed result. */ 33 | public static function result (result :Try) :Future { 34 | return new Future(new TryValue(result)); 35 | } 36 | 37 | /** Returns a future containing an Array of all success results from {@code futures} if all of 38 | * the futures complete successfully, or a MultiFailureException aggregating all failures, if 39 | * any of the futures fail. */ 40 | public static function sequence (futures :Array) :Future { 41 | // if we're passed an empty list of futures, succeed immediately with an empty list 42 | if (futures.length == 0) { 43 | return Future.success([]); 44 | } 45 | 46 | const pseq :Promise = new Promise(); 47 | const seq :Sequencer = new Sequencer(pseq, futures.length); 48 | for (var ii :int = 0, len :int = futures.length; ii < len; ++ii) { 49 | var future :Future = futures[ii]; 50 | future.onComplete(addToSequenceCallback(seq, ii)); 51 | } 52 | return pseq; 53 | } 54 | 55 | /** Returns a future containing a list of all success results from {@code futures}. Any failure 56 | * results are simply omitted from the list. The success results are also in no particular 57 | * order. If all of {@code futures} fail, the resulting list will be empty. */ 58 | public static function collect (futures :Array) :Future { // Future> 59 | // if we're passed an empty list of futures, succeed immediately with an empty list 60 | if (futures.length == 0) { 61 | return Future.success([]); 62 | } 63 | 64 | const pseq :Promise = new Promise(); 65 | const results :Array = []; 66 | var remain :int = futures.length; 67 | for each (var future :Future in futures) { 68 | future.onComplete(function (result :Try) :void { 69 | if (result.isSuccess) { 70 | results.push(result.value); 71 | } 72 | if (--remain == 0) { 73 | pseq.succeed(results); 74 | } 75 | }); 76 | } 77 | return pseq; 78 | } 79 | 80 | /** Causes {@code slot} to be notified if/when this future is completed with success. If it has 81 | * already suceeded, the slot will be notified immediately. 82 | * @return this future for chaining. */ 83 | public function onSuccess (slot :Function) :Future { 84 | var result :Try = _result.get(); 85 | if (result == null) { 86 | _result.connect(function (result :Try) :void { 87 | if (result.isSuccess) { 88 | call(slot, result.value); 89 | } 90 | }); 91 | } else if (result.isSuccess) { 92 | call(slot, result.value); 93 | } 94 | return this; 95 | } 96 | 97 | /** Causes {@code slot} to be notified if/when this future is completed with failure. If it has 98 | * already failed, the slot will be notified immediately. 99 | * @return this future for chaining. */ 100 | public function onFailure (slot :Function) :Future { 101 | var result :Try = _result.get(); 102 | if (result == null) { 103 | _result.connect(function (result :Try) :void { 104 | if (result.isFailure) { 105 | call(slot, result.failure); 106 | } 107 | }); 108 | } else if (result.isFailure) { 109 | call(slot, result.failure); 110 | } 111 | return this; 112 | } 113 | 114 | /** Causes {@code slot} to be notified when this future is completed. If it has already 115 | * completed, the slot will be notified immediately. 116 | * @return this future for chaining. */ 117 | public function onComplete (slot :Function) :Future { 118 | var result :Try = _result.get(); 119 | if (result == null) { 120 | _result.connect(slot); 121 | } else { 122 | call(slot, result); 123 | } 124 | return this; 125 | } 126 | 127 | /** Returns a value that indicates whether this future has completed. */ 128 | public function get isComplete () :BoolView { 129 | if (_isComplete == null) { 130 | _isComplete = _result.mapToBool(Functions.NON_NULL); 131 | } 132 | return _isComplete; 133 | } 134 | 135 | /** Convenience method to {@link ValueView#connectNotify} {@code slot} to {@link #isComplete}. 136 | * This is useful for binding the disabled state of UI elements to this future's completeness 137 | * (i.e. disabled while the future is incomplete, then reenabled when it is completed). 138 | * @return this future for chaining. */ 139 | public function bindComplete (slot :Function) :Future { 140 | this.isComplete.connectNotify(slot); 141 | return this; 142 | } 143 | 144 | /** Maps the value of a successful result using {@link #func} upon arrival. */ 145 | public function map (func :Function) :Future { 146 | // we'd use Try.lift here but we have to handle the special case where our try is null, 147 | // meaning we haven't completed yet; it would be weird if Try.lift did that 148 | return new Future(_result.mapToTry(function (result :Try) :Try { 149 | return (result == null ? null : result.map(func)); 150 | })); 151 | } 152 | 153 | /** Maps a successful result to a new result using {@link #func} when it arrives. Failure on 154 | * the original result or the mapped result are both dispatched to the mapped result. This is 155 | * useful for chaining asynchronous actions. It's also known as monadic bind. */ 156 | public function flatMap (func :Function) :Future { 157 | const mapped :TryValue = new TryValue(); 158 | _result.connectNotify(function (result :Try) :void { 159 | if (result == null) { 160 | return; // source future not yet complete; nothing to do 161 | } else if (result.isFailure) { 162 | mapped.value = Try.failure(result.failure); 163 | } else { 164 | var mappedResult :Future; 165 | try { 166 | mappedResult = func(result.value); 167 | } catch (e :Error) { 168 | mapped.value = Try.failure(e); 169 | return; 170 | } 171 | mappedResult.onComplete(mapped.slot); 172 | } 173 | }); 174 | return new Future(mapped); 175 | } 176 | 177 | public function Future (result :TryView) { 178 | _result = result; 179 | } 180 | 181 | protected static function addToSequenceCallback (seq :Sequencer, idx :int) :Function { 182 | return function (result :Try) :void { 183 | seq.onResult(idx, result); 184 | }; 185 | } 186 | 187 | protected static function call (f :Function, arg :Object) :void { 188 | if (f.length == 1) { 189 | f(arg); 190 | } else { 191 | f(); 192 | } 193 | } 194 | 195 | protected var _result :TryView; 196 | protected var _isComplete :BoolView; 197 | } 198 | 199 | } 200 | 201 | import react.MultiFailureError; 202 | import react.Promise; 203 | import react.Try; 204 | 205 | class Sequencer { 206 | public function Sequencer (pseq :Promise, count :int) { 207 | _pseq = pseq; 208 | _results = []; 209 | _results.length = count; 210 | _remain = count; 211 | } 212 | 213 | public function onResult (idx :int, result :Try) :void { 214 | if (result.isSuccess) { 215 | _results[idx] = result.value; 216 | } else { 217 | if (_error == null) { 218 | _error = new MultiFailureError(); 219 | } 220 | _error.addFailure(result.failure); 221 | } 222 | 223 | if (--_remain == 0) { 224 | if (_error != null) { 225 | _pseq.fail(_error); 226 | } else { 227 | _pseq.succeed(_results); 228 | } 229 | } 230 | } 231 | 232 | protected var _pseq :Promise; // Promise 233 | protected var _results :Array; 234 | protected var _remain :int; 235 | protected var _error :MultiFailureError; 236 | } 237 | -------------------------------------------------------------------------------- /src/test/as/react/FutureTest.as: -------------------------------------------------------------------------------- 1 | // 2 | // React-Test 3 | 4 | package react { 5 | 6 | public class FutureTest 7 | { 8 | public function testImmediate () :void { 9 | const counter :FutureCounter = new FutureCounter(); 10 | 11 | const success :Future = Future.success("Yay!"); 12 | counter.bind(success); 13 | counter.check("immediate succeed", 1, 0, 1); 14 | 15 | const failure :Future = Future.failure(new Error("Boo!")); 16 | counter.bind(failure); 17 | counter.check("immediate failure", 0, 1, 1); 18 | } 19 | 20 | public function testDeferred () :void { 21 | const counter :FutureCounter = new FutureCounter(); 22 | 23 | const success :Promise = new Promise(); 24 | counter.bind(success); 25 | counter.check("before succeed", 0, 0, 0); 26 | success.succeed("Yay!"); 27 | counter.check("after succeed", 1, 0, 1); 28 | 29 | const failure :Promise = new Promise(); 30 | counter.bind(failure); 31 | counter.check("before fail", 0, 0, 0); 32 | failure.fail(new Error("Boo!")); 33 | counter.check("after fail", 0, 1, 1); 34 | 35 | Assert.isFalse(success.hasConnections); 36 | Assert.isFalse(failure.hasConnections); 37 | } 38 | 39 | public function testMappedImmediate () :void { 40 | const counter :FutureCounter = new FutureCounter(); 41 | 42 | const success :Future = Future.success("Yay!"); 43 | counter.bind(success.map(Functions.NON_NULL)); 44 | counter.check("immediate succeed", 1, 0, 1); 45 | 46 | const failure :Future = Future.failure(new Error("Boo!")); 47 | counter.bind(failure.map(Functions.NON_NULL)); 48 | counter.check("immediate failure", 0, 1, 1); 49 | 50 | // Throwing an error in map() should result in a failure 51 | const successToFail :Future = Future.success("Yay!"); 52 | counter.bind(successToFail.map(function (value :*) :String { 53 | throw new Error("Boo!"); 54 | })); 55 | counter.check("succeed-map-failure", 0, 1, 1); 56 | } 57 | 58 | public function testMappedDeferred () :void { 59 | const counter :FutureCounter = new FutureCounter(); 60 | 61 | const success :Promise = new Promise(); 62 | counter.bind(success.map(Functions.NON_NULL)); 63 | counter.check("before succeed", 0, 0, 0); 64 | success.succeed("Yay!"); 65 | counter.check("after succeed", 1, 0, 1); 66 | 67 | const failure :Promise = new Promise(); 68 | counter.bind(failure.map(Functions.NON_NULL)); 69 | counter.check("before fail", 0, 0, 0); 70 | failure.fail(new Error("Boo!")); 71 | counter.check("after fail", 0, 1, 1); 72 | 73 | Assert.isFalse(success.hasConnections); 74 | Assert.isFalse(failure.hasConnections); 75 | } 76 | 77 | public function testFlatMapValues () :void { 78 | var finalValue :String = null; 79 | var p1 :Promise = new Promise(); 80 | p1.flatMap(function (value :String) :Future { 81 | return Future.success(value + "Bar"); 82 | }) 83 | .flatMap(function (value :String) :Future { 84 | return Future.success(value + "Baz"); 85 | }) 86 | .onSuccess(function (value :String) :void { 87 | finalValue = value; 88 | }); 89 | 90 | p1.succeed("Foo"); 91 | Assert.equals(finalValue, "FooBarBaz"); 92 | } 93 | 94 | public function testFlatMappedImmediate () :void { 95 | const scounter :FutureCounter = new FutureCounter(); 96 | const fcounter :FutureCounter = new FutureCounter(); 97 | 98 | const success :Future = Future.success("Yay!"); 99 | scounter.bind(success.flatMap(SUCCESS_MAP)); 100 | fcounter.bind(success.flatMap(FAIL_MAP)); 101 | scounter.check("immediate success/success", 1, 0, 1); 102 | fcounter.check("immediate success/failure", 0, 1, 1); 103 | 104 | const failure :Future = Future.failure(new Error("Boo!")); 105 | scounter.bind(failure.flatMap(SUCCESS_MAP)); 106 | fcounter.bind(failure.flatMap(FAIL_MAP)); 107 | scounter.check("immediate failure/success", 0, 1, 1); 108 | scounter.check("immediate failure/failure", 0, 1, 1); 109 | 110 | // Throwing an error in flatmap() should result in a failed Future 111 | const success2 :Future = Future.success("Yay2!"); 112 | var finalValue :String = null; 113 | const successToFlatMapError :Future = success2.flatMap(function (result :*) :Future { 114 | throw new Error("FlatMap failure!"); 115 | }).onFailure(function (err :Error) :void { 116 | finalValue = err.message; 117 | }); 118 | 119 | fcounter.bind(successToFlatMapError); 120 | fcounter.check("immediate success/thrown-error", 0, 1, 1); 121 | Assert.equals(finalValue, "FlatMap failure!"); 122 | } 123 | 124 | public function testFlatMappedDeferred () :void { 125 | const scounter :FutureCounter = new FutureCounter(); 126 | const fcounter :FutureCounter = new FutureCounter(); 127 | 128 | const success :Promise = new Promise(); 129 | scounter.bind(success.flatMap(SUCCESS_MAP)); 130 | scounter.check("before succeed/succeed", 0, 0, 0); 131 | fcounter.bind(success.flatMap(FAIL_MAP)); 132 | fcounter.check("before succeed/fail", 0, 0, 0); 133 | success.succeed("Yay!"); 134 | scounter.check("after succeed/succeed", 1, 0, 1); 135 | fcounter.check("after succeed/fail", 0, 1, 1); 136 | 137 | const failure :Promise = new Promise(); 138 | scounter.bind(failure.flatMap(SUCCESS_MAP)); 139 | fcounter.bind(failure.flatMap(FAIL_MAP)); 140 | scounter.check("before fail/success", 0, 0, 0); 141 | fcounter.check("before fail/failure", 0, 0, 0); 142 | failure.fail(new Error("Boo!")); 143 | scounter.check("after fail/success", 0, 1, 1); 144 | fcounter.check("after fail/failure", 0, 1, 1); 145 | 146 | Assert.isFalse(success.hasConnections); 147 | Assert.isFalse(failure.hasConnections); 148 | } 149 | 150 | public function testFlatMappedDoubleDeferred () :void { 151 | const scounter :FutureCounter = new FutureCounter(); 152 | const fcounter :FutureCounter = new FutureCounter(); 153 | 154 | { const success :Promise = new Promise(); 155 | const innerSuccessSuccess :Promise = new Promise(); 156 | scounter.bind(success.flatMap(function (value :String) :Future { 157 | return innerSuccessSuccess; 158 | })); 159 | scounter.check("before succeed/succeed", 0, 0, 0); 160 | const innerSuccessFailure :Promise = new Promise(); 161 | fcounter.bind(success.flatMap(function (value :String) :Future { 162 | return innerSuccessFailure; 163 | })); 164 | fcounter.check("before succeed/fail", 0, 0, 0); 165 | 166 | success.succeed("Yay!"); 167 | scounter.check("after first succeed/succeed", 0, 0, 0); 168 | fcounter.check("after first succeed/fail", 0, 0, 0); 169 | innerSuccessSuccess.succeed(true); 170 | scounter.check("after second succeed/succeed", 1, 0, 1); 171 | innerSuccessFailure.fail(new Error("Boo hoo!")); 172 | fcounter.check("after second succeed/fail", 0, 1, 1); 173 | 174 | Assert.isFalse(success.hasConnections); 175 | Assert.isFalse(innerSuccessSuccess.hasConnections); 176 | Assert.isFalse(innerSuccessFailure.hasConnections); 177 | } 178 | 179 | { 180 | const failure :Promise = new Promise(); 181 | const innerFailureSuccess :Promise = new Promise(); 182 | scounter.bind(failure.flatMap(function (value :String) :Future { 183 | return innerFailureSuccess; 184 | })); 185 | scounter.check("before fail/succeed", 0, 0, 0); 186 | const innerFailureFailure :Promise = new Promise(); 187 | fcounter.bind(failure.flatMap(function (value :String) :Future { 188 | return innerFailureFailure; 189 | })); 190 | fcounter.check("before fail/fail", 0, 0, 0); 191 | 192 | failure.fail(new Error("Boo!")); 193 | scounter.check("after first fail/succeed", 0, 1, 1); 194 | fcounter.check("after first fail/fail", 0, 1, 1); 195 | innerFailureSuccess.succeed(true); 196 | scounter.check("after second fail/succeed", 0, 1, 1); 197 | innerFailureFailure.fail(new Error("Is this thing on?")); 198 | fcounter.check("after second fail/fail", 0, 1, 1); 199 | 200 | Assert.isFalse(failure.hasConnections); 201 | Assert.isFalse(innerFailureSuccess.hasConnections); 202 | Assert.isFalse(innerFailureFailure.hasConnections); 203 | } 204 | } 205 | 206 | public function testSequenceImmediate () :void { 207 | const counter :FutureCounter = new FutureCounter(); 208 | 209 | const success1 :Future = Future.success("Yay 1!"); 210 | const success2 :Future = Future.success("Yay 2!"); 211 | 212 | const failure1 :Future = Future.failure(new Error("Boo 1!")); 213 | const failure2 :Future = Future.failure(new Error("Boo 2!")); 214 | 215 | const sucseq :Future = Future.sequence([success1, success2]); 216 | counter.bind(sucseq); 217 | sucseq.onSuccess(function (results :Array) :void { 218 | Assert.equals(results.length, 2); 219 | Assert.equals(results[0], "Yay 1!"); 220 | Assert.equals(results[1], "Yay 2!"); 221 | }); 222 | counter.check("immediate seq success/success", 1, 0, 1); 223 | 224 | counter.bind(Future.sequence([success1, failure1])); 225 | counter.check("immediate seq success/failure", 0, 1, 1); 226 | 227 | counter.bind(Future.sequence([failure1, success2])); 228 | counter.check("immediate seq failure/success", 0, 1, 1); 229 | 230 | counter.bind(Future.sequence([failure1, failure2])); 231 | counter.check("immediate seq failure/failure", 0, 1, 1); 232 | } 233 | 234 | public function testSequenceDeferred () :void { 235 | const counter :FutureCounter = new FutureCounter(); 236 | 237 | const success1 :Promise = new Promise(), success2 :Promise = new Promise(); 238 | const failure1 :Promise = new Promise(), failure2 :Promise = new Promise(); 239 | 240 | const suc2seq :Future = Future.sequence([success1, success2]); 241 | counter.bind(suc2seq); 242 | suc2seq.onSuccess(function (results :Array) :void { 243 | Assert.equals(results.length, 2); 244 | Assert.equals(results[0], "Yay 1!"); 245 | Assert.equals(results[1], "Yay 2!"); 246 | }); 247 | counter.check("before seq succeed/succeed", 0, 0, 0); 248 | success1.succeed("Yay 1!"); 249 | success2.succeed("Yay 2!"); 250 | counter.check("after seq succeed/succeed", 1, 0, 1); 251 | 252 | const sucfailseq :Future = Future.sequence([success1, failure1]); 253 | sucfailseq.onFailure(function (cause :Error) :void { 254 | Assert.isTrue(cause is MultiFailureError); 255 | Assert.equals("1 failure: Boo 1!", cause.message); 256 | }); 257 | counter.bind(sucfailseq); 258 | counter.check("before seq succeed/fail", 0, 0, 0); 259 | failure1.fail(new Error("Boo 1!")); 260 | counter.check("after seq succeed/fail", 0, 1, 1); 261 | 262 | const failsucseq :Future = Future.sequence([failure1, success2]); 263 | failsucseq.onFailure(function (cause :Error) :void { 264 | Assert.isTrue(cause is MultiFailureError); 265 | Assert.equals("1 failure: Boo 1!", cause.message); 266 | }); 267 | counter.bind(failsucseq); 268 | counter.check("after seq fail/succeed", 0, 1, 1); 269 | 270 | const fail2seq :Future = Future.sequence([failure1, failure2]); 271 | fail2seq.onFailure(function (cause :Error) :void { 272 | Assert.isTrue(cause is MultiFailureError); 273 | Assert.equals("2 failures: Boo 1!, Boo 2!", MultiFailureError(cause).getMessage()); 274 | }); 275 | counter.bind(fail2seq); 276 | counter.check("before seq fail/fail", 0, 0, 0); 277 | failure2.fail(new Error("Boo 2!")); 278 | counter.check("after seq fail/fail", 0, 1, 1); 279 | } 280 | 281 | public function testCollectEmpty () :void { 282 | var counter :FutureCounter = new FutureCounter(); 283 | var seq :Future = Future.collect([]); 284 | counter.bind(seq); 285 | counter.check("collect empty list succeeds", 1, 0, 1); 286 | } 287 | 288 | public function testCollectImmediate () :void { 289 | var counter :FutureCounter = new FutureCounter(); 290 | 291 | const success1 :Future = Future.success("Yay 1!"); 292 | const success2 :Future = Future.success("Yay 2!"); 293 | 294 | const failure1 :Future = Future.failure(new Error("Boo 1!")); 295 | const failure2 :Future = Future.failure(new Error("Boo 2!")); 296 | 297 | const sucCollect :Future = Future.collect([success1, success2]); 298 | counter.bind(sucCollect); 299 | sucCollect.onSuccess(function (results :Array) :void { 300 | Assert.equals(results.length, 2); 301 | }); 302 | counter.check("immediate collect success/success", 1, 0, 1); 303 | 304 | counter.bind(Future.collect([success1, failure1])); 305 | counter.check("immediate collect success/failure", 1, 0, 1); 306 | 307 | counter.bind(Future.collect([failure1, success2])); 308 | counter.check("immediate collect failure/success", 1, 0, 1); 309 | 310 | counter.bind(Future.collect([failure1, failure2])); 311 | counter.check("immediate collect failure/failure", 1, 0, 1); 312 | } 313 | 314 | public function testCollectDeferred () :void { 315 | const counter :FutureCounter = new FutureCounter(); 316 | 317 | const success1 :Promise = new Promise(), success2 :Promise = new Promise(); 318 | const failure1 :Promise = new Promise(), failure2 :Promise = new Promise(); 319 | 320 | const suc2Collect :Future = Future.collect([success1, success2]); 321 | counter.bind(suc2Collect); 322 | suc2Collect.onSuccess(function (results :Array) :void { 323 | Assert.equals(results.length, 2); 324 | }); 325 | counter.check("before seq succeed/succeed", 0, 0, 0); 326 | success1.succeed("Yay 1!"); 327 | success2.succeed("Yay 2!"); 328 | counter.check("after seq succeed/succeed", 1, 0, 1); 329 | 330 | const sucfailCollect :Future = Future.collect([success1, failure1]); 331 | sucfailCollect.onSuccess(function (results :Array) :void { 332 | Assert.equals(results.length, 1); 333 | Assert.equals(results[0], "Yay 1!"); 334 | }); 335 | counter.bind(sucfailCollect); 336 | counter.check("before seq succeed/fail", 0, 0, 0); 337 | failure1.fail(new Error("Boo 1!")); 338 | counter.check("after seq succeed/fail", 1, 0, 1); 339 | 340 | const failsucCollect :Future = Future.collect([failure1, success2]); 341 | failsucCollect.onSuccess(function (results :Array) :void { 342 | Assert.equals(results.length, 1); 343 | Assert.equals(results[0], "Yay 2!"); 344 | }); 345 | counter.bind(failsucCollect); 346 | counter.check("after seq fail/succeed", 1, 0, 1); 347 | 348 | const fail2Collect :Future = Future.collect([failure1, failure2]); 349 | fail2Collect.onSuccess(function (results :Array) :void { 350 | Assert.equals(results.length, 0); 351 | }); 352 | counter.bind(fail2Collect); 353 | counter.check("before seq fail/fail", 0, 0, 0); 354 | failure2.fail(new Error("Boo 2!")); 355 | counter.check("after seq fail/fail", 1, 0, 1); 356 | } 357 | 358 | protected static function SUCCESS_MAP (value :String) :Future { 359 | return Future.success(value != null); 360 | } 361 | 362 | protected static function FAIL_MAP (value :String) :Future { 363 | return Future.failure(new Error("Barzle!")); 364 | } 365 | } 366 | 367 | } 368 | 369 | import react.Counter; 370 | import react.Future; 371 | 372 | class FutureCounter { 373 | public const successes :Counter = new Counter(); 374 | public const failures :Counter = new Counter(); 375 | public const completes :Counter = new Counter(); 376 | 377 | public function bind (future :Future) :void { 378 | reset(); 379 | future.onSuccess(successes.slot); 380 | future.onFailure(failures.slot); 381 | future.onComplete(completes.slot); 382 | } 383 | 384 | public function check (state :String, scount :int, fcount :int, ccount :int) :void { 385 | successes.assertTriggered(scount, "Successes " + state); 386 | failures.assertTriggered(fcount, "Failures " + state); 387 | completes.assertTriggered(ccount, "Completes " + state); 388 | } 389 | 390 | public function reset () :void { 391 | successes.reset(); 392 | failures.reset(); 393 | completes.reset(); 394 | } 395 | } 396 | --------------------------------------------------------------------------------