hxbolts is a port of a "tasks" component from java library named Bolts. A task is kind of like a JavaScript Promise, but with different API.
hxbolts is not a binding to the java library, but pure-haxe cross-platform port.
Important note: original java library is about keeping long-running operations out of the UI thread, but current version of hxbolts is more about transforming async callback hell into nice looking code.
Stable release:
haxelib install hxbolts
Development version:
haxelib git hxbolts https://github.com/restorer/hxbolts.git
To use all power of hxbolts, at first we need to boltify some existing function with callbacks. For example, let we have a function:
function doSomethingAsync(
param : String,
onSuccessCallback : Int -> Void,
onFailureCallback : String -> Void,
onCancelledCallback : Void -> Void
) : Void {
...
}
To boltify it create a TaskCompletionSource
.
This object will let you create a new Task, and control whether it gets marked as finished or cancelled.
After you create a Task
, you'll need to call setResult
, setError
, or setCancelled
to trigger its continuations.
function doSomethingAsyncTask(param : String) : Task<Int> {
var tcs = new TaskCompletionSource<Int>();
doSomethingAsync(param, function(result : Int) : Void {
tcs.setResult(result);
}, function(failureReason : String) : Void {
tcs.setError(failureReason);
}, function() : Void {
tcs.setCancelled();
});
return tcs.task;
}
That's all 😃 Now you can chain tasks together and do all async stuff in easy manner.
Another example:
function loadTextFromUrlAsync(url : String) : Task<String> {
var tcs = new TaskCompletionSource<String>();
var urlLoader = new URLLoader();
var onLoaderComplete = function(_) : Void {
tcs.setResult(Std.string(urlLoader.data));
};
var onLoaderError = function(e : Event) : Void {
tcs.setError(e);
};
urlLoader.dataFormat = URLLoaderDataFormat.TEXT;
urlLoader.addEventListener(Event.COMPLETE, onLoaderComplete);
urlLoader.addEventListener(IOErrorEvent.IO_ERROR, onLoaderError);
urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onLoaderError);
try {
urlLoader.load(new URLRequest(url));
} catch (e : Dynamic) {
tcs.setError(e);
}
return tcs.task;
}
Every Task
has a method named continueWith
which takes a continuation function, which called when the task is complete.
You can then inspect the task to check if it was successful and to get its result.
loadTextFromUrlAsync("http://domain.tld").continueWith(function(task : Task<String>) : Nothing {
if (task.isCancelled) {
// the load was cancelled.
// NB. loadTextFromUrlAsync() mentioned earlier is used just for illustration,
// actually it doesn't support cancelling.
} else if (task.isFaulted) {
// the load failed.
var error : Dynamic = task.error;
} else {
// the text was loaded successfully.
trace(task.result);
}
return null;
});
Tasks are strongly-typed using generics, so getting the syntax right can be a little tricky at first. Let's look closer at the types involved with an example.
function getStringAsync() : Task<String> {
// Let's suppose getIntAsync() returns a Task<Int>.
return getIntAsync().continueWith(function(task : Task<Int>) : String {
// This Continuation is a function which takes an Integer as input,
// and provides a String as output. It must take an Integer because
// that's what was returned from the previous Task.
// The Task getIntAsync() returned is passed to this function for convenience.
var number : Int = task.result;
return 'The number = ${number}';
});
}
In many cases, you only want to do more work if the previous task was successful, and propagate any errors or cancellations to be dealt with later.
To do this, use the onSuccess
method instead of continueWith
.
loadTextFromUrlAsync("http://domain.tld").onSuccess(function(task : Task<String>) : Nothing {
// the text was loaded successfully.
trace(task.result);
return null;
});
Tasks are a little bit magical, in that they let you chain them without nesting.
If you use continueWithTask
instead of continueWith
, then you can return a new task.
The task returned by continueWithTask
will not be considered finished until the new task returned from within continueWithTask
is.
This lets you perform multiple actions without incurring the pyramid code you would get with callbacks.
Likewise, onSuccessTask
is a version of onSuccess
that returns a new task.
So, use continueWith
/onSuccess
to do more synchronous work, or continueWithTask
/onSuccessTask
to do more asynchronous work.
loadTextFromUrlAsync("http://domain.tld").onSuccessTask(function(task : Task<String>) : Task<Array<Int>> {
return storeResultOnServerAndReturnResultCodeListAsync(task.result);
}).onSuccessTask(function(task : Task<Array<Int>>) : Task<String> {
return loadTextFromUrlAsync("http://anotherdomain.tld/index.php?ret=" + task.result.join("-"));
}).onSuccessTask(function(task : Task<String>) : Task<CustomResultObject> {
return storeAnotherResultOnServerAndReturnCustomResultObjectAsync(task.result);
}).onSuccess(function(task : Task<CustomResultObject>) : Nothing {
// Everything is done!
return null;
});
By carefully choosing whether to call continueWith
or onSuccess
, you can control how errors are propagated in your application.
Using continueWith
lets you handle errors by transforming them or dealing with them.
You can think of failed tasks kind of like throwing an exception.
In fact, if you throw an exception inside a continuation, the resulting task will be faulted with that exception.
loadTextFromUrlAsync("http://domain.tld").onSuccessTask(function(task : Task<String>) : Task<Array<Int>> {
// Force this callback to fail.
throw "There was an error.";
}).onSuccessTask(function(task : Task<Array<Int>>) : Task<String> {
// Now this continuation will be skipped.
return loadTextFromUrlAsync("http://anotherdomain.tld/index.php?ret=" + task.result.join("-"));
}).continueWithTask(function(task : Task<String>) : Task<CustomResultObject> {
if (task.isFaulted()) {
// This error handler WILL be called.
// The error will be "There was an error."
// Let's handle the error by returning a new value.
// The task will be completed with null as its value.
return null;
}
// This will also be skipped.
return storeAnotherResultOnServerAndReturnCustomResultObjectAsync(task.result);
}).onSuccess(function(task : Task<CustomResultObject>) : Nothing {
// Everything is done! This gets called.
// The task's result is null.
return null;
});
It's often convenient to have a long chain of success callbacks with only one error handler at the end.
You already know that tasks can be created using TaskCompletionSource
.
But if you know the result of a task at the time it is created, there are some convenience methods you can use.
var successful : Task<String> = Task.forResult("The good result.");
var failed : Task<String> = Task.forError("An error message.");
There is also
call
function that help you create tasks from straight blocks of code.call
tries to execute its block immediately or at specified executor. However in current version of hxbolts it is not really usable due to missing of good background executors.
Tasks are convenient when you want to do a series of tasks in a row, each one waiting for the previous to finish. For example, imagine you want to delete all of the comments on your blog.
findCommentsAsync({ post: 123 }).continueWithTask(function(resultTask : Task<Array<CommentInfo>>) : Task<Nothing> {
// Create a trivial completed task as a base case.
var task : Task<Nothing> = Task.forResult(null);
for (commentInfo in resultTask.result) {
// For each item, extend the task with a function to delete the item.
task = task.continueWithTask(function(_) : Task<Nothing> {
// Return a task that will be marked as completed when the delete is finished.
return deleteCommentAsync(commentInfo);
});
}
return task;
}).continueWith(function(task : Task<Nothing>) : Nothing {
if (task.isSuccessed) {
// Every comment was deleted.
}
return null;
});
You can also perform several tasks in parallel, using the whenAll
method.
You can start multiple operations at once, and use Task.whenAll
to create a new task that will be marked as completed when all of its input tasks are completed.
The new task will be successful only if all of the passed-in tasks succeed.
Performing operations in parallel will be faster than doing them serially, but may consume more system resources and bandwidth.
findCommentsAsync({ post: 123 }).continueWithTask(function(resultTask : Task<Array<CommentInfo>>) : Task<Nothing> {
// Collect one task for each delete into an array.
var tasks = new Array<Task<Nothing>>();
for (commentInfo in resultTask.result) {
// Start this delete immediately and add its task to the list.
tasks.push(deleteCommentAsync(commentInfo));
}
return Task.whenAll(tasks);
}).continueWith(function(task : Task<Nothing>) : Nothing {
if (task.isSuccessed) {
// Every comment was deleted.
}
return null;
});
Prior to Haxe 3.3 it was possible to use Void
as return value (in reality, depending on target, it can be null
or undefined
or something else). hxbolts used this nice hack to have more clean code.
Starting with Haxe 3.3 it is not possible to do this anymore - HaxeFoundation/haxe#5519 (I agree with this decision). But we need some type for void-values:
enum Nothing {
nothing;
}
You can find void-types like that in other haxelibs (for example Nil
in thx.core), but to reduce dependency on other libs hxbolts has it own void-type.
This type can have only 2 values: Nothing.nothing
and null
. hxbolts ignore these values, so you can use anything you want. Internally null
is used (just like original java library).
P.S. Nice to have void-type in standard Haxe library.
Product still is in development (but not active).
Feature | Support status |
---|---|
New features | Yes |
Non-critical bugfixes | Yes |
Critical bugfixes | Yes |
Pull requests | Accepted (after review) |
Issues | Monitored |
Estimated end-of-life | Up to 2019 |
- Support for
CancellationToken
-
@async
/@await
on top of this library