Trying task

  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 library hasn’t been updated for 2 years, and it seems that it’s designed for other purposes and not for those for whom I was trying to use it. Anyway, let’s try, because this lib (v1.0.6) has incredible haxelib downloads – 11586 (wow!).

TL;DR – 🙁 As I thought, it is designed for other purposes, and can’t be used as Promises.

Let’s begin

Several things to note:

  1. Unlike promises, task has no two states (resolved or rejected), so I used haxe.ds.Either to store resolved value in Right and rejected in Left.
  2. Task must have unique identifier.
  3. Task must be added to task list (either global, named TaskManager or local).
  4. By default task is synchronous. To make it asynchronous, you can either pass additional argument to addTask() function or pass magical function (created by TaskList.handleEvent()) to task arguments.
function fetchText(url : String) : Task {
    var task : Task = null;

    var handler = function(completeHandler : Dynamic) : Void {
        var urlLoader = new URLLoader();

        var onLoaderError = function(e : Event) : Void {
            task.result = Either.Left(e.type);
            completeHandler(true);
        };

        urlLoader.addEventListener(Event.COMPLETE, function(_) : Void {
            task.result = Either.Right(Std.string(urlLoader.data));
            completeHandler(true);
        });

        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) {
            Timer.delay(function() : Void {
                task.result = Either.Left(Std.string(e));
                completeHandler(true);
            }, 0);
        }
    };

    task = new Task('fetchText:${url}', handler, [TaskList.handleEvent()]);
    taskList.addTask(task);

    return task;
}

More notes:

  1. No strict typing 🙁
  2. By default TaskList.handleEvent() creates handler that accepts one argument, so I pass true (but actually it never used).
  3. There is tricky part where async task must return value immediately (in my case when exception occurred). It is possible to do it right, but to simplify code I used Timer.delay() to force it to be async.

Huh.

function fetchJson(url : String) : Task {
    var fetchTextTask = fetchText(url);

    var task = new Task('fetchJson:${url}', function() : Either<String, DynamicExt> {
        switch ((cast fetchTextTask.result : Either<String, String>)) {
            case Left(v):
                return Either.Left(v);

            case Right(v):
                try {
                    return Either.Right(Json.parse(v));
                } catch (e : Dynamic) {
                    return Either.Left(Std.string(e));
                }
        }
    });

    taskList.addTask(task, [fetchTextTask]);
    return task;
}

To chain tasks I can pass an array of sub-tasks which must be completed before running the task as second argument of addTask() function. Also, because this library doesn’t have task state (resolved or rejected) I must manually handle rejected value for each task.

fetchBitmapData() is too similar with fetchText(), so I wouldn’t show it now and in future posts.

function sendApiRequest(method : String) : Task {
    var fetchJsonTask = fetchJson('http://some.api/${method}');

    var task = new Task('sendApiRequest:${method}', function() : Either<String, DynamicExt> {
        switch ((cast fetchJsonTask.result : Either<String, DynamicExt>)) {
            case Left(v):
                return Either.Left(v);

            case Right(v):
                if (v["error"] != null) {
                    return Either.Left(v["error"].asString());
                } else {
                    return Either.Right(v);
                }
        }
    });

    taskList.addTask(task, [fetchJsonTask]);
    return task;
}

Pretty straightforward. But the next function will use more tricks:

function syncState() : Task {
    var task : Task = null;
    var sendApiRequestTask = sendApiRequest("sync-state");

    var handler = function(completeHandler : Dynamic) : Void {
        switch ((cast sendApiRequestTask.result : Either<String, DynamicExt>)) {
            case Left(v): {
                Timer.delay(function() : Void {
                    task.result = false;
                    completeHandler(true);
                }, 0);
            }

            case Right(v): {
                updateStateFromTheResponse(v);

                var taskResult = true;
                var subTasks : Array<Task> = [];

                if (v.exists("playerAvatarUrl")) {
                    var fetchBitmapDataTask = fetchBitmapData(v["playerAvatarUrl"].asString());

                    var subTask = new Task('setPlayerAvatar', function() : Void {
                        switch ((cast fetchBitmapDataTask.result : Either<String, BitmapData>)) {
                            case Left(v):
                                taskResult = false;

                            case Right(v):
                                setPlayerAvatar(v);
                        }
                    });

                    subTasks.push(subTask);
                    taskList.addTask(subTask, [fetchBitmapDataTask]);
                }

                if (v.exists("opponentAvatarUrl")) {
                    var fetchBitmapDataTask = fetchBitmapData(v["opponentAvatarUrl"].asString());

                    var subTask = new Task('setOpponentAvatar', function() : Void {
                        switch ((cast fetchBitmapDataTask.result : Either<String, BitmapData>)) {
                            case Left(v):
                                taskResult = false;

                            case Right(v):
                                setOpponentAvatar(v);
                        }
                    });

                    subTasks.push(subTask);
                    taskList.addTask(subTask, [fetchBitmapDataTask]);
                }

                taskList.addTask(new Task('syncState:sub', function() : Void {
                    task.result = taskResult;
                    completeHandler(true);
                }), subTasks);
            }
        }
    };

    task = new Task('syncState', handler, [TaskList.handleEvent()]);
    taskList.addTask(task, [sendApiRequestTask]);

    return task;
}

Initially I try to add sub-tasks to task.prerequisiteTasks, but it didn’t work, so I create fake syncState:sub task to handle completion of sub-tasks.

And finally:

function doTheTask() : Void {
    taskList = new TaskList();

    showLoader();
    var syncStateTask = syncState();

    taskList.addTask(new Task('main', function() : Void {
        hideLoader();

        if ((cast syncStateTask.result : Bool)) {
            onTaskSuccessed();
        } else {
            showErrorPopup();
        }
    }), [syncStateTask]);
}

Notice: each time I create new TaskList instance.

I can’t use the same TaskList instance (or use global task list inside TaskManager class) because all completed tasks stored in special “cache” of completed tasks, and adding task with the same id can lead to unpredictable effects.

Pros

  • Incredible downloads count;
  • Easy to understand, very simple code (only 3 classes).

Cons

  • Seems to be outdated and not supported anymore;
  • No strict typing (most arguments have Dynamic type), which leads to runtime errors instead of compiler errors;
  • It is designed for other purposes, and can’t be used as Promises;
  • You can finish task by task id, you can check task state by task id, but there is no way to get task result by task id (actually such function exists, but it is private).

In my humble opinion

  • Personally I don’t like idea of named tasks.

Conclusion

Indeed, it can simplify asynchronous tasks, but I think you should give a chance to other libraries on haxelib.

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.