A class hierarchy wraps every possible result from an AsyncJobScope:
AsyncResult.Completed → if AsyncJobScope finished its execution.
AsyncResult.Completed.Success → if AsyncJobScope finished its execution successfully.
AsyncResult.Completed.Failure → if AsyncJobScope finished its execution but with an error.
AsyncResult.Cancelled → if AsyncJobScope was cancelled.
Every time an AsyncScope is executed, a List<AsyncResult> is created that collects all registered AsyncJobScope results in the same order each job was added.
This approach uses async expression that accepts only one trailing lambda and should be used only when low-level details are required.
Since sealed-classes are used to represent AsyncResult, it is quite simple to evaluate each one:
TurboScope.execute {
async {
// adding 3 jobs for execution
asyncJob<Int>{...}
asyncJob<Boolean>{...}
asyncJob<Double>{...}
}.let { r ->
when (r[0]) {
AsyncResult.Cancelled -> ...
is AsyncResult.Completed.Success -> ...
is AsyncResult.Completed.Failure -> ...
}
when (r[1]) {
AsyncResult.Cancelled -> ...
is AsyncResult.Completed.Success -> ...
is AsyncResult.Completed.Failure -> ...
}
when (r[2]) {
AsyncResult.Cancelled -> ...
is AsyncResult.Completed.Success -> ...
is AsyncResult.Completed.Failure -> ...
}
}
}
But using this traditional approach is quite demanding since when expressions are required for every single result.
To simplify this, io.turbodsl offers extension-functions for List<AsyncResult>:
List<AsyncResult>.allCompleted() → true when all items are AsyncResult.Completed.
List<AsyncResult>.anyCompleted() → true if contains at least one AsyncResult.Completed.
List<AsyncResult>.allSuccess() → true when all items are AsyncResult.Completed.Success.
List<AsyncResult>.anySuccess() → true if contains at least one AsyncResult.Completed.Success.
List<AsyncResult>.allFailure() → true when all items are AsyncResult.Completed.Failure.
List<AsyncResult>.anyFailure() → true if contains at least one AsyncResult.Completed.Failure.
List<AsyncResult>.allCancelled() → true when all items are AsyncResult.Cancelled.
List<AsyncResult>.anyCancelled() → true if contains at least one AsyncResult.Cancelled.
TurboScope.execute {
async {
// adding 3 jobs for execution
asyncJob<Int>{...}
asyncJob<Boolean>{...}
asyncJob<Double>{...}
}.let { r ->
if (r.allSuccess()) {
// all r items are succeses
} else {
// handler errors / cancellations
}
}
}
Similarly, extension-functions for AsyncResult simplify how to check the result state:
AsyncResult.isCompleted() → true if the result item is AsyncResult.Completed - either a Success or Failure.
AsyncResult.isSuccess() → true if the result item is AsyncResult.Completed.Success.
AsyncResult.isFailure() → true if the result item is AsyncResult.Completed.Failure.
AsyncResult.isCancelled() → true if the result item is AsyncResult.Cancelled.
Using these functions is the same as using a when expression:
TurboScope.execute {
async {
// adding 3 jobs for execution
asyncJob<Int>{...}
asyncJob<Boolean>{...}
asyncJob<Double>{...}
}.let { r ->
when {
r[0].isSuccess() -> {...}
r[0].isFailure() -> {...}
r[0].isCancelled() -> {...}
}
:
}
}
So far all these functions provide mechanisms to check the state of an AsyncJobScope result. To access the related value, you must use specific attributes:
AsyncResult.Completed.Success → exposes attribute value, which references the actual return-value from the AsyncJobScope.
AsyncResult.Completed.Failure → exposes attribute error, which references the actual error / exception causing AsyncJobScope to fail.
AsyncResult.Cancelled → exposes nothing, since the AsyncJobScope was cancelled, thus, its result is undetermined.
TurboScope.execute {
async {
// adding 3 jobs for execution
asyncJob<Int>{...}
asyncJob<Boolean>{...}
asyncJob<Double>{...}
}.let { r ->
when (val r0 = r[0]) {
AsyncResult.Cancelled -> {
// r0 does not provide any reference to the determine state
}
is AsyncResult.Completed.Success -> {
// r0.value -> requires casting
}
is AsyncResult.Completed.Failure -> {
// r0.error -> it's a Throwable
}
}
:
}
}
Putting everything together:
TurboScope.execute {
async {
// adding 3 jobs for execution
asyncJob<Int>{...}
asyncJob<Boolean>{...}
asyncJob<Double>{...}
}.let { r ->
if (r.allSuccess()) {
// Safe to cast to Success, since all are Successes
// But, you need to know the data-type for each r[index]
val r0 = ((r[0] as AsyncResult.Completed.Success).value as Int)
val r1 = ((r[1] as AsyncResult.Completed.Success).value as Boolean)
val r2 = ((r[2] as AsyncResult.Completed.Success).value as Double)
// Process results
} else {
// handler errors / cancellations
// At least one r[i] is an AsyncResult.Completed.Failure
}
}
}
This is still quite extensive and evaluating raw results should be done in specific scenarios. It is better to use async with AsyncResultScope.
Similar to the previous approach, but async expression using two lambdas, build and block (which is the trailing lambda):
TurboScope.execute {
async<String>(
build = {
// "this" is an instance of AsyncScope
// adding 3 jobs for execution
asyncJob<Int>{...}
asyncJob<Boolean>{...}
asyncJob<Double>{...}
},
) { ok, r ->
// "this" is an instance of AsyncResultScope
if (ok) {
// Safe to cast to Success, since all are Successes
// But, you need to know the data-type for each r[index]
val r0 = (r[0].success() as Int)
val r1 = (r[1].success() as Boolean)
val r2 = (r[2].success() as Double)
// Process results and return a String value
} else {
// handler errors / cancellations
// At least one r[i] is an AsyncResult.Completed.Failure
// Throw error, or return a String value
}
}
}
AsyncResultScope receives 2 arguments:
Boolean → true if all results are AsyncResult.Completed.Success.
List<AsyncResult> → a list containing all AsyncJobScope results.
Additionally, AsyncResultScope exposes 2 extension-functions:
AsyncResult.success() → should be used only when the result is a AsyncResult.Completed.Success.
AsyncResult.failure() → should be used only when the result is a AsyncResult.Completed.Failure.
This approach uses async expression that accepts between 2 and 10 jobs and it is the preferred method when the number of asynchronous-jobs is known and static.
There are 9 different async functions accepting 2 to 10 AsyncJobScopes:
TurboScope.execute<String> {
async(
// adding 3 jobs for execution
job1 = asyncJob<Int>{...},
job2 = asyncJob<Boolean>{...},
job3 = asyncJob<Double>{...}
) { ok, r1, r2, r3 ->
// "this" is an instance of AsyncResultScope
if (ok) {
// No need to cast to Success, and safe to call success()
// No need to cast to data-type - each r-index "knows" the type to cast into
val r0 = r1.success() // r0 is an Int
val r1 = r2.success() // r1 is a Boolean
val r2 = r3.success() // r2 is a Double
// Process results and return a String value
} else {
// handler errors / cancellations
// At least one r[i] is an AsyncResult.Completed.Failure
// Throw error, or return a String value
}
}
}
AsyncResultScope receives n+1 arguments:
Boolean → true if all results are AsyncResult.Completed.Success.
r<index> → AsyncResult instance for each job.
Using the extension-function success() on each "r" does not require explicit casting.
Comparing the previous async versions, there's a huge advantage when the number of AsyncJobs is known.