Kovenant Core Usage

part of kovenant-core


Task

The easiest way to create a Promise is by using task, e.g.

val promise = task { foo() }

This will execute function foo() asynchronously and immediately returns a Promise<V, Exception> where V is the inferred return type of foo(). If foo() completes successful the Promise is resolved as successful. Any Exception from foo() is considered a failure.

task dispatches the work on the workerContext.


Deferred

With a Deferred<V,E> you can take matters in your own hand. Instead of relying on task for resolving or rejecting a promise it's up to the developer. You obtain a deferred object by simply calling deferred<V, E>(). From there you can either resolve(V) or reject(E) the deferred. This can only be set once and by default trying to resolve or reject multiple times throws an Exception. The behaviour can be configured though.

From a Deferred<V,E> we can obtain the companion Promise<V, E> as easy as deferred.promise. This promise can of course passed around as much as you want, just like any promise. Just keep the deferred to yourself.

Example

fun foo() {
    val deferred = deferred<String,Exception>()
    handlePromise(deferred.promise)

    deferred.resolve("Hello World")
//    deferred.reject(Exception("Hello exceptional World"))
}
fun handlePromise(promise: Promise<String, Exception>) {
    promise success {
        msg -> println(msg)
    }
    promise fail {
        e -> println(e)
    }
}

Cancelable Deferred

By default the promise returned by a Deferred is not a CancelablePromise. Simply because there is no way to tell the owner of the Promise that a cancel has been requested. If we however provide a callback to the deferred function we are able to cancel. What cancelling means is of course up to the one owning the Deferred.

val deferred = deferred<Int, String> {
    //callback method to receive notification
    //when cancel is requested
    println("I'm cancelled by $it")
}

//Convenience method for trying to cancel promises
Kovenant.cancel(deferred.promise, "test method")

Callbacks

A Promise<V, E> allows you to add 3 types of callbacks:

When a promise resolves successfully all current registered success callbacks are fired. Of course, any new callback will be fired as well. If the promise has failed all the current registered fail callbacks will be fired. And again, any new callback will be fired as well.

No matter what the result of the promise is, success or failure, always get fired always upon completion.

Please read the extra note on the order of calling success, fail and always.

val promise = task {
    //mimicking those long running operations with:
    1 + 1
}

promise success {
    //called on succesfull completion of the promise
    println("result: $it")  
}

promise fail {
    //called when an exceptions has occurred
    println("that's weird ${it.message}") 
}

promise always {
    //no matter what result we get, this is always called once.
}

Chaining

All callback registering functions return this Promise, thus previous example can be written without those intermediate variables

task {
    //some (long running) operation, or just:
    1 + 1
} success {
    //called when no exceptions have occurred
    println("result: $it")  
} fail {
    //called when an exceptions has occurred
    println("that's weird ${it.message}") 
} always {
    //no matter what result we get, this is always called once.
}

DispatcherContext

By default the callbacks are executed on the callback DispatcherContext that is associated with this Promise. But you can also provide your own DispatcherContext for a specific callback.

val dispatcherContext = //...

task {
    foo()
}.success(dispatcherContext) {
    bar()
}

Multiple Success stories

You don't have to limit yourself to registering just one callback. You can add multiple success, fail and always actions to one single promise. Thus a promise can be passed around and anybody who's interested can get notified. Previously registered callbacks don't get overwritten. Every callback will be called once and only once upon completion.

task {
    1 + 1
} success {
    println("1")    
} success {
    println("2")    
} success {
    println("3")    
}

Execution order

The order of execution of the callbacks depends greatly on the underlying callback DispatcherContext. Kovenant guarantees that callbacks are offered to the DispatcherContext in the same order they were added to the Promise. The default callback DispatcherContext also maintains this order. So by default all callbacks are executed in the same order they were added.

The default behaviour can easily be broken though. For instance, if you configure the callbackDispatcher to operate with 2 threads the order of execution becomes undefined. Not knowing the order of execution can have some undesired side effects. For example, consider:

val firstRef = AtomicReference<String>()
val secondRef = AtomicReference<String>()

val first = task { "hello" } success {
    firstRef.set(it)
}
val second = task { "world" } success {
    secondRef.set(it)
}

all (first, second) success {
    println("${firstRef.get()} ${secondRef.get()}")
}

If we don't have guarantees about the order of the callbacks the above example simply won't work. This is because the all function also relies on callbacks on the first and second promise. So without order guarantees the successcallback of all might just execute before the success callbacks of the first and second promise. So don't just blindly change the callback DispatcherContext without actually understanding what you are doing.


Then

then operates similar to task except that it takes the output from a previous Promise as its input. This allows you to chain units of work.

task {
    fib(20)
} then {
    "fib(20) = $it, and fib(21) = (${fib(21)})"
} success {
    println(it)
}

Any Exception thrown from any of the steps in the chain of promises results in every next promises to be resolved as failed. The work of then is executed by the workerContext.


ThenApply

thenApply operates similar to then except that it takes the output from a previous Promise as its input as an extension function. The previous example would thus be:

task {
    fib(20)
} thenApply {
    "fib(20) = $this, and fib(21) = (${fib(21)})"
} success {
    println(it)
}

Get

Sometimes you need to just wait for a result, this is what get() does. It blocks the calling thread until the result is available. Returning the success value if resolved successful. If promise resolved as a failure an Exception is thrown. If the error value of the promise is an Exception then that is thrown directly, otherwise a FailureException is thrown with the error value wrapped in it.

val fib20 : Int = task { fib(20) }.get()

Note that implementors should override this function because the fallback methods are far from efficient


isDone

The functions isDone(), isFailure() and isSuccess() simply tells you if this promise is resolved and whether it is successful or has failed. This comes in handy in combination with get().

Note that implementors should override these functions because the fallback functions are far from efficient


Lazy Promise

Kovenant provides a lazyPromise property delegate similar to Kotlin's standard library Delegates.lazy {}. The difference with the standard library version is that initialization happens by an task operation and thus effectively on a background thread. This is particularly useful on platforms like Android where you want to avoid initialization on the UI Thread.

val expensiveResource by lazyPromise {
    println("init promise")
    ExpensiveResource()
}

fun main(args: Array<String>) {
    println("start program")

    expensiveResource thenApply {
        "Got [$value]"
    } success {
        println(it)
    }
}


class ExpensiveResource {
    val value :String = "result"
}

Of

In order align with existing libraries and code you can create promises of existing values with of, ofSuccess and ofFail.

// Success promise with inferred value type
// and Exception as fail type 
Promise.of(13)

// Failed promise with explicit types
Promise.ofFail<String, Int>(13)

// Successful promise with explicit types
Promise.ofSuccess<String, Int>("thirteen")

All

Sometimes you want to make sure that multiple promises are done before proceeding. With all this can be achieved. all<V,Exception> takes a vararg of Promise<V,Exception>s and returns a Promise<List<V>, Exception>. The returned Promise is considered a success if all of the provided Promises are successful. If any fail the whole promise fails. The returned List<V> contains the items in the same order as the Promises provided to all. If you want to mix promises of different types you probably want to take a look at combine

By default all tries to cancel all provided promises if one fails. If you don't want your promises to be cancelled you can set cancelOthersOnFail = false. See cancel for more on this topic.

val promises = Array(10) { n ->
    task {
        Pair(n, fib(n))
    }
}

all(*promises) success {
    it forEach { pair -> println("fib(${pair.first}) = ${pair.second}") }
} always {
    println("All ${promises.size()} promises are done.")
}

Any

Sometimes you want to make sure that at least one of multiple promises is done before proceeding. With any this can be achieved. any<V,Exception> takes a vararg of Promise<V,Exception>s and returns a Promise<V, List<Exception>>. The returned Promise is considered a success if any of the provided Promises is successful. If all fail the whole promise fails. The returned List<Exception> contains the items in the same order as the Promises provided to any.

By default any tries to cancel all provided promises if one succeeds. If you don't want your promises to be cancelled you can set cancelOthersOnSuccess = false. See cancel for more on this topic.

val promises = Array(10) { n ->
    task {
        while(!Thread.currentThread().isInterrupted()) {
            val luckyNumber = Random(System.currentTimeMillis() * n).nextInt(100)
            if (luckyNumber == 7) break
        }
        "Promise number $n won!"
    }
}

any (*promises) success { msg ->
    println(msg)
    println()

    promises forEachIndexed { n, p ->
        p.fail { println("promise[$n] was canceled") }
        p.success { println("promise[$n] finished") }
    }
} 

Cancel

Any Promise that implements CancelablePromise allows itself to be cancelled. By default the promises returned from task, then and thenApply are CancelablePromises.

Cancelling a promises is quite similar to Deferred.reject as it finishes the promises as failed. Thus the callbacks fail and always are still executed. Cancel does also try to prevent the promised work from ever being scheduled. If the promised work is already running it gets interrupted (when using default dispatchers).


Void

There are those times where just knowing that something has either failed or succeeded is enough information. So for the ultimate "on a need to know basis" Kovenant provides three methods that hide the results:


unwrap

Unwraps any nested Promise. By default the returned Promise will operate on the same Context as its parent Promise, no matter what the Context of the nested Promise is. If you want the resulting promise to operate on a different Context you can provide one.

Function tries to be as efficient as possible in cases where this or the nested Promise is already resolved. This means that this function might or might not create a new Promise, it all depends on the current state.

val nested = Promise.of(Promise.of(42))
val promise = nested.unwrap()
promise success {
    println(it)
}

withContext

Returns a Promise operating on the provided Context. This function might return the same instance of the Promise or a new one depending whether the Context of the Promise and the provided Promise match.

val p = Promise.of(42).withContext(Kovenant.context)