Promises in AS3

by devon_o
♥2 | Line 362 | Modified 2013-03-16 19:42:24 | MIT License
play

ActionScript3 source code

/**
 * Copyright devon_o ( http://wonderfl.net/user/devon_o )
 * MIT License ( http://www.opensource.org/licenses/mit-license.php )
 * Downloaded from: http://wonderfl.net/c/qxZA
 */

package 
{
    
import com.bit101.components.PushButton;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.text.TextFieldAutoSize;
import flash.text.TextFormat;
import flash.utils.setTimeout;
import flash.text.TextField;

/**
 * A look at promises in AS3
 * 
 * Some reading
 * http://en.wikipedia.org/wiki/Futures_and_promises
 * http://blogs.msdn.com/b/ie/archive/2011/09/11/asynchronous-programming-in-javascript-with-promises.aspx
 * https://gist.github.com/darscan/4519372
 * 
 * @author devon o.
 */

[SWF(width='465', height='465', backgroundColor='#000000', frameRate='60')]
public class Main extends Sprite 
{
    
    private var game:Game;
    private var assets:Assets;
    private var player:Player;
    
    private var startButton:PushButton;
    
    public function Main():void 
    {
        if (stage) init();
        else addEventListener(Event.ADDED_TO_STAGE, init);
    }
    
    private function init(event:Event = null):void 
    {
        removeEventListener(Event.ADDED_TO_STAGE, init);
        
        this.game    = new Game();
        this.assets    = new Assets();
        this.player    = new Player();
        
        initOutputText();
        initStartButton();
    }
    
    private function initOutputText():void
    {
        var tf:TextField = new TextField();
        tf.width = stage.stageWidth;
        tf.autoSize = TextFieldAutoSize.LEFT;
        tf.multiline = true;
        tf.wordWrap = true;
        tf.defaultTextFormat = new TextFormat("_sans", 11, 0x000000);
        tf.x = tf.y = 10;
        addChild(tf);
        Utils.output = tf;
    }
    
    private function initStartButton():void
    {
        this.startButton = new PushButton(this, 300, 10, "Start Sequence", startLoadSequence);
    }
    
    private function startLoadSequence(event:MouseEvent):void
    {
        this.startButton.enabled = false;
        
        Utils.output.text = "";
        
        // The beauty of the Promise construct: Asynchronous event/object chaining in crystal clarity.
        this.assets.getAssets()
            .then( this.player.getPlayerInfo )
            .then( this.game.getHighScores )
            .then( this.game.init )
            .then( onLoadSequenceComplete, onLoadSequenceError );
    }
    
    /**
     * Called after successful completion of entire sequence
     */
    private function onLoadSequenceComplete(arg:*):void
    {
        Utils.addOutputText("Load sequence complete. Starting game.");
        
        this.game.start();
        
        this.startButton.enabled = true;
    }
    
    /**
     * Called if any step of the sequence fails
     */
    private function onLoadSequenceError(err:Error):void
    {
        Utils.addOutputText("Load sequence failed. Reason = " + err.message);
        
        this.startButton.enabled = true;
    }
    
}
    
}


import flash.utils.setTimeout;
class Assets
{
    /**
     * Class to simulate the loading of external assets
     */
    
    private var graphicsDef:Deferred;
    private var soundsDef:Deferred;
    
    public function Assets() { }
    
    public function getAssets():Promise
    {
        // A nested Promise
        return getGraphics()
               .then(getSounds)
               .then(onAssetLoadSuccess, onAssetLoadErr);
    }
    
    private function getGraphics():Promise
    {
        Utils.addOutputText("Loading game graphics...");
        
        this.graphicsDef = new Deferred(onGraphicsLoadSuccess, onGraphicsLoadErr);
        
        // Here you would create a loader instance and load your graphics.
        // a Loader error (http, security, io, etc.) would call rejectGraphics
        // a Loader success would call resolveGraphics
        var f:Function = Utils.randomFunction(resolveGraphics, rejectGraphics);
        
        // simulate asynchronous loading time
        var time:int = int(Utils.randRange(3000, 500));
        setTimeout(f, time);
        return this.graphicsDef;
    }
    
    private function getSounds(arg:* = null):Promise
    {
        Utils.addOutputText("Loading game sounds...");
        
        this.soundsDef = new Deferred(onSoundsLoadSuccess, onSoundsLoadErr);
        
        // Here you would create a loader instance and load your sounds.
        // a Loader error (http, security, io, etc. would call rejectSounds
        // a Loader success would call resolveSounds
        var f:Function = Utils.randomFunction(resolveSounds, rejectSounds);
        
        // simulate asynchronous loading time
        var time:int = int(Utils.randRange(3000, 500));
        setTimeout(f, time);
        return this.soundsDef;
    }
    
    private function rejectGraphics():void
    {
        this.graphicsDef.reject(new Error("Could not load graphics"));
    }
    
    private function resolveGraphics():void
    {
        this.graphicsDef.resolve(null);
    }
    
    private function onGraphicsLoadSuccess(arg:* = null):void
    {
        Utils.addOutputText("Graphics loaded successfully.");
    }
    
    private function onGraphicsLoadErr(err:Error):void
    {
        throw(err);
    }
    
    private function rejectSounds():void
    {
        this.soundsDef.reject(new Error("Could not load sounds"));
    }
    
    private function resolveSounds():void
    {
        this.soundsDef.resolve(null);
    }
    
    private function onSoundsLoadSuccess(arg:* = null):void
    {
        Utils.addOutputText("Sounds loaded successfully.");
    }
    
    private function onSoundsLoadErr(err:Error):void
    {
        throw(err);
    }
    
    
    private function onAssetLoadSuccess(arg:* = null):GameDataObject
    {
        var gdo:GameDataObject = new GameDataObject();
        gdo.assets = this;
        
        return gdo;
    }
    
    private function onAssetLoadErr(err:Error):void
    {
        throw(err);
    }
}


class Game
{
    /**
     * Class to simulate a Game object
     */
    public function Game() { }
    
    private var playerInfoDef:Deferred;
    private var highScoreDef:Deferred;
    
    public function getHighScores(gdo:GameDataObject):Promise
    {
        Utils.addOutputText("Loading high score data for PlayerID: " + gdo.player.playerID + "...");
        
        this.highScoreDef = new Deferred(onHighScoreLoadSuccess, onHighScoreLoadErr);
        
        // Here you would create a loader instance and load your high scores from a database or whatever.
        // a Loader error (http, security, io, etc.) would call rejectScores
        // a Loader success would call resolveScores
        var f:Function = Utils.randomFunction(resolveScores, rejectScores);
        
        // simulate asynchronous loading time
        var time:int = int(Utils.randRange(3000, 500));
        setTimeout(f, time, gdo);
        
        return this.highScoreDef;
    }
    
    private function rejectScores(gdo:GameDataObject):void
    {
        this.highScoreDef.reject(new Error("Could not load high scores"));
    }
    
    private function resolveScores(gdo:GameDataObject):void
    {
        this.highScoreDef.resolve(gdo);
    }
    
    private function onHighScoreLoadSuccess(gdo:GameDataObject):GameDataObject
    {
        Utils.addOutputText("High score data loaded successfully");
        gdo.game = this;
        return gdo;
    }
    
    private function onHighScoreLoadErr(err:Error):void
    {
        throw(err);
    }
    
    public function init(gdo:GameDataObject):void
    {
        Utils.addOutputText("Game is initializing with  " + gdo.assets + ", " + gdo.game + ", " + gdo.player);
    }
    
    public function start():void
    {
        Utils.addOutputText("Game is starting.");
    }
}


class Player
{
    /**
     * Class to simulate a Player object
     */
    
    public var playerID:String;
     
    private var playerInfoDef:Deferred;
    
    public function Player() { }
    
    public function getPlayerInfo(gdo:GameDataObject):Promise
    {
        Utils.addOutputText("Loading Player Info data...");
        
        this.playerInfoDef = new Deferred(onPlayerInfoLoadSuccess, onPlayerInfoLoadErr);
        
        // Here you would create a loader instance and load your player data.
        // a Loader error (http, security, io, etc.) would call rejectPlayerInfo
        // a Loader success would call resolvePlayerInfo
        var f:Function = Utils.randomFunction(resolvePlayerInfo, rejectPlayerInfo);
        
        // simulate asynchronous loading time
        var time:int = int(Utils.randRange(3000, 500));
        setTimeout(f, time, gdo);
        
        return this.playerInfoDef;
    }
    
    private function rejectPlayerInfo(gdo:GameDataObject):void
    {
        this.playerInfoDef.reject(new Error("Could not load player info"));
    }
    
    private function resolvePlayerInfo(gdo:GameDataObject):void
    {
        this.playerInfoDef.resolve(gdo);
    }
    
    private function onPlayerInfoLoadSuccess(gdo:GameDataObject):GameDataObject
    {
        Utils.addOutputText("Player Info data loaded successfully");
        this.playerID = "12345";
        gdo.player = this;
        return gdo;
    }
    
    private function onPlayerInfoLoadErr(err:Error):void
    {
        throw(err);
    }
}

class GameDataObject
{
    public var game:Game;
    public var player:Player;
    public var assets:Assets;
}

import flash.text.TextField;
class Utils
{
    /**
     * Some helper methods
     */
    
    public static var output:TextField;
    
    public static function randomFunction(succeedFunc:Function, failFunc:Function, chanceOfFailure:Number = .20):Function
    {
        return Math.random() < chanceOfFailure ? failFunc : succeedFunc;
    }
    
    public static function randRange(max:Number, min:Number = 0, decimals:int = 0):Number 
    {
        if (min > max) return NaN;
        var rand:Number = Math.random() * (max - min) + min;
        var d:Number = Math.pow(10, decimals);
        return ~~((d * rand) + 0.5) / d;
    }
    
    public static function addOutputText(text:String):void
    {
        var str:String = output.text;
        str += text + "\n------------\n";
        output.text = str;
    }
}








/**
 * Everything below here from: https://gist.github.com/darscan/4519372
 */

class Deferred implements Promise
{

    private const pending:Array = [];

    private var processed:Boolean;

    private var completed:Boolean;

    private var completionAction:String;

    private var completionValue:*;

    private var onResolved:Function;

    private var onRejected:Function;

    public function Deferred(onResolved:Function = null, onRejected:Function = null)
    {
        this.onResolved = onResolved;
        this.onRejected = onRejected || throwError;
    }

    public function resolve(result:*):void
    {
        processed || process(onResolved, result);
    }

    public function reject(error:*):void
    {
        processed || process(onRejected, error);
    }

    public function then(onResolved:Function = null, onRejected:Function = null):Promise
    {
        if (onResolved != null || onRejected != null)
        {
            const deferred:Deferred = new Deferred(onResolved, onRejected);
            NextTick.call(schedule, [deferred]);
            return deferred;
        }
        return this;
    }

    private function throwError(error:*):void
    {
        throw error;
    }

    private function schedule(deferred:Deferred):void
    {
        pending.push(deferred);
        completed && propagate();
    }

    private function propagate():void
    {
        for each (var deferred:Deferred in pending)
            deferred[completionAction](completionValue);
        pending.length = 0;
    }

    private function complete(action:String, value:*):void
    {
        onResolved = null;
        onRejected = null;
        completionAction = action;
        completionValue = value;
        completed = true;
        propagate();
    }

    private function completeResolved(result:*):void
    {
        complete('resolve', result);
    }

    private function completeRejected(error:*):void
    {
        complete('reject', error);
    }
    
    private function process(closure:Function, value:*):void
    {
        processed = true;
        try
        {
            closure && (value = closure(value));
            value is Promise
                ? Promise(value).then(completeResolved, completeRejected)
                : completeResolved(value);
        }
        catch (error:*)
        {
            completeRejected(error);
        }
    }
}



import flash.display.Sprite;
import flash.events.Event;

class NextTick
{
    private static const SPR:Sprite = new Sprite();

    private static const Q:Array = [];

    public static function call(closure:Function, args:Array = null):void
    {
        Q.push(new Scope(closure, args));
        Q.length == 1 && SPR.addEventListener(Event.ENTER_FRAME, run);
    }

    private static function run(e:Event):void
    {
        SPR.removeEventListener(Event.ENTER_FRAME, run);
        for each (var scope:Scope in Q.splice(0))
            scope.execute();
    }
}

 
class Scope
{
    private var _closure:Function;
 
    private var _args:Array;
 
    public function Scope(closure:Function, args:Array)
    {
        _closure = closure;
        _args = args;
    }
 
    public function execute():void
    {
        _args ? _closure.apply(null, _args) : _closure();
    }
}

interface Promise
{
    function then(onResolved:Function = null, onRejected:Function = null):Promise;
}