Trying thx.promise

  1. Why hxbolts?
  2. Trying promhx
  3. Trying thx.promise
  4. Trying task
  5. Trying continuation
  6. Trying async
  7. Trying hext-flow
  8. Finally, hxbolts

This time I will try to use thx.promise v0.5.1. Pretty much haxelib downloads: 412.

TL;DR – 😐 Easy to use, strongly typed with clean code. There are one thing (in my point of view it is an error, but I can provide arguments for both sides) in Promise.all(), which can be easily fixed.

Let’s begin

As usual, I start with fetchText() function. thx.promise use ES6 style for creating promise. Not in my taste, but work well.

function fetchText(url : String) : Promise<String> {
    return Promise.create(function(resolve : String -> Void, reject : Error -> Void) : Void {
        var urlLoader = new URLLoader();

        var onLoaderError = function(e : Event) : Void {
            reject(Error.fromDynamic(e.type));
        };

        urlLoader.addEventListener(Event.COMPLETE, function(_) : Void {
            resolve(Std.string(urlLoader.data));
        });

        urlLoader.addEventListener(IOErrorEvent.IO_ERROR, onLoaderError);
        urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onLoaderError);

        try {
            urlLoader.dataFormat = URLLoaderDataFormat.TEXT;
            urlLoader.load(new URLRequest(url));
        } catch (e : Dynamic) {
            reject(Error.fromDynamic(Std.string(e)));
        }
    });
}

With success() and mapSuccess() I can easily chain promises:

function fetchJson(url : String) : Promise<DynamicExt> {
    return fetchText(url).mapSuccess(function(result : String) : DynamicExt {
        return cast Json.parse(result);
    });
}

Next:

function fetchBitmapData(url : String) : Promise<BitmapData> {
    return Promise.create(function(resolve : BitmapData -> Void, reject : Error -> Void) : Void {
        var loader = new Loader();

        var onLoaderError = function(e : Event) : Void {
            reject(Error.fromDynamic(Std.string(e)));
        };

        loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(_) : Void {
            resolve((cast loader.content:Bitmap).bitmapData);
        });

        loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onLoaderError);
        loader.contentLoaderInfo.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onLoaderError);

        try {
            loader.load(new URLRequest(url), new LoaderContext(true));
        } catch (e : Dynamic) {
            reject(Error.fromDynamic(Std.string(e)));
        }
    });
}

Just like fetchText().

function sendApiRequest(method : String) : Promise<DynamicExt> {
    return fetchJson('http://some.api/${method}').mapSuccessPromise(function(result : DynamicExt) : Promise<DynamicExt> {
        if (result["error"] != null) {
            return Promise.error(Error.fromDynamic(result["error"]));
        }

        return Promise.value(result);
    });
}

thx.promise didn’t catch exceptions, so I just return pre-filled promises for error and value.

function syncState() : Promise<Nil> {
    return sendApiRequest("sync-state").mapSuccessPromise(function(result : DynamicExt) : Promise<Nil> {
        var subPromises = new Array<Promise<Nil>>();
        updateStateFromTheResponse(result);

        if (result.exists("playerAvatarUrl")) {
            subPromises.push(fetchBitmapData(result["playerAvatarUrl"].asString()).mapSuccess(function(bmd : BitmapData) : Nil {
                setPlayerAvatar(bmd);
                return Nil.nil;
            }));
        }

        if (result.exists("opponentAvatarUrl")) {
            subPromises.push(fetchBitmapData(result["opponentAvatarUrl"].asString()).mapSuccess(function(bmd : BitmapData) : Nil {
                setOpponentAvatar(bmd);
                return Nil.nil;
            }));
        }

        return reallyAfterAll(subPromises);
    });
}

function doTheTask() : Void {
    showLoader();

    syncState().then(function(result : Result<Nil, Error>) : Void {
        hideLoader();

        if (result.isSuccess) {
            onTaskSuccessed();
        } else {
            showErrorPopup();
        }
    });
}

Easy. But pay attention to reallyAfterAll() function. Initially I use Promise.afterAll(), but it has the same bug as in promhx – when one of sub-promises rejected, result promise rejected before all sub-promises completed. It may work (and make sense) when you promises are pure functions, and don’t modify any global state, but in other case this can lead to subtle bugs.

Luckily it can be easily fixed:

static function reallyAfterAll(arr : Array<Promise<Dynamic>>) : Promise<Nil> {
    return Promise.create(function(resolve, reject) {
        reallyAll(arr).either(
            function(_) { resolve(Nil.nil); },
            reject
        );
    });
}

static function reallyAll<T>(arr : Array<Promise<T>>) : Promise<Array<T>> {
    if (arr.length == 0) {
        return Promise.value([]);
    }

    return Promise.create(function(resolve, reject) {
        var results = [];
        var counter = 0;
        var errors = [];

        arr.mapi(function(p, i) {
            p.either(function(value) {
                if (errors.length == 0) {
                    results[i] = value;
                }

                counter++;

                if (counter == arr.length) {
                    if (errors.length != 0) {
                        reject(Error.fromDynamic(errors));
                    } else {
                        resolve(results);
                    }
                }
            }, function(err) {
                errors.push(err);
                counter++;

                if (counter == arr.length) {
                    reject(Error.fromDynamic(errors));
                }
            });
        });
    });
}

The library has clean code, and I find what I must fix and wrote fix just in 5 minutes.

Pros

  • Strong library with clean code;
  • Only 3 files (not counting thx.core), easy to understand;
  • Just works.

Cons

  • Keep in mind nuance about Promise.all() function.

Conclusion

Good library, but in my point of view, Promise.all() should work differently.

Source code

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.