Liking cljdoc? Tell your friends :D

promesa - promise library for clojure and clojurescript

Introduction

A promise library for Clojure and ClojureScript.

On the JVM platform is built on top of completable futures (requires jdk8) and on JS platform is built on top of bluebird promise library.

Project Maturity

Since promesa is a young project there may be some API breakage.

Install

Just include that in your depencency vector on project.clj:

[funcool/promesa "1.1.1"]

This package requires JDK8 if you are using it on the JVM platform and all enviroments that bluebird library supports on the JS platform.

User guide

Introduction

The promise is the abstraction that represents the result of an asynchronous operation that has the notion of error.

This is a list of possible states of one promise:

  • resolved: means that the promise contains a value.

  • rejected: means thet the promise contains an error.

  • pending: means that the promise does not have value.

The promise can be considered done when is resolved or rejected.

Creating a promise

It there several different ways to create a promise instance:

Example creating already resolved promise instances from plain values.
(require '[promesa.core :as p])

;; Create a fulfilled promise
(p/promise 1)
;; => #<Promise [1]>

If a promise function receives a plain value, it returns a resolved promise with the provided plain value. If it receives an instance of Error, it returns a rejected promise.

Also, it accepts a factory callback that receives two callable parameters: resolve and reject. So you can use one or other to resolve or reject the promise respectively.

Example creating promise instance using a factory.
(p/promise (fn [resolve reject]
             (resolve 1)))

In both platforms the promise factory function is executed synchrously and making it blocking or not blocking is user responsability.

Promise Chaining

The most common way to apply a function to a promise value (or in other word chain its execution) is using the well known map function:

(def result (->> (p/promise 1)
                 (p/map inc)))

@result     ; only on the jvm
;; => 2

For people more coming from js world, it there are also then function that works in the same way as map but with parameters inverted:

(def result (-> (p/promise 1)
                (p/then inc)))

@result     ; only on the jvm
;; => 2

If you want to apply multiple functions, instead of using multiple times then or map, you can just use chain:

(def result (-> (p/promise 1)
                (p/chain inc inc inc))

@result     ; only on the jvm
;; => 4

It there also mapcat function that allows remove one neesting level of promises, and is pretty useful if the function that you want to apply to promise also return a promise instance instead of value:

(def incp #(p/resolved (inc %)))

(def result (->> (p/promise 1)
                 (p/mapcat incp)))

@result     ; only on the jvm
;; => 2

The mapcat function is only useful in the JVM platform. In JS platform the map function already flattens the result magically (because the underlying implementation does that).

Async/Await Syntax

The promesa library comes with convenient sugar syntax that allows you create a compositions that looks like synchronous code with very clojure familiar let syntax:

Example using async/await syntax.
(require '[promesa.core :as p])

(defn sleep-promise
  [wait]
  (p/promise (fn [resolve reject]
               (p/schedule wait #(resolve wait)))))

(def result
  (p/alet [x (p/await (sleep-promise 42))
           y (p/await (sleep-promise 41))
           z 2]
    (+ x y z)))

@result     ; only on the jvm
;; => 85

ClojureScript usage: if you’re getting "Should be only used in alet macro." error, perhaps you forgot to add :include-macros true to the require.

The alet macro behaves identical to the let with the exception that it always return a promise and allows mark async operations with await placeholder making it looks like synchronous opetation.

If an error occurs at any step the entire composition will be short-circuited, returning exceptionally resolved promise.

If you are not familiar with async/await syntax, you can read more about it here.

Error handling

One of the advantages of using promise abstraction is that it natively has a notion of error, so you don’t need reinvent it. If some of the computations of the composed promise chain/pipeline raises an exception, that one is automatically propagated to the last promise making the effect of short-circuiting.

Let see an example:

(-> (p/promise (ex-info "error" nil))
    (p/catch (fn [error]
               (.log js/console error))))

The catch function adds a new handler to the promise chain that will be called when any of the previous promises in the chain are rejected or an exception is raised. The catch function also returns a promise that will be resolved or rejected depending on that will happen inside the catch hanlder.

If you prefer map like parameters order, it there err function (and error alias) that works in same way as catch but has the parameters like map:

(->> (p/promise (ex-info "error" nil))
     (p/error (fn [error]
                (.log js/console error))))

On the JVM platform the reject value is mandatory to be an instance of Throwable but in JS platform it can by any value.

Branching

For adding both success and error handlers to a promise at the same time you can use the branch function:

(p/branch a-promise
          (fn [v]
            (println "Ok" v))
          (fn [err]
            (println err)))

Working with collections

In some circumstances you will want wait a completion of few promises at same time, and promesa also provides helpers for that.

Imagine that you have a collection of promises and you want to wait until all of them are resolved. This can be done using the all combinator:

(let [p (p/all [(do-some-io)
                (do-some-other-io)])]
  (p/then p (fn [[result1 result2]]
              (do-something-with-results result1 result2))))

It there are also circumstances where you only want arbitrary select of the first resolved promise. For this case, you can use the any combinator:

(let [p (p/any [(p/delay 100 1)
                (p/delay 200 2)
                (p/delay 120 3)])]
  (p/then p (fn [x]
              (.log js/console "The first one finished: " x))))

Delays and timeouts.

JavaScript due its nature, does not allow you to block or sleep. But with promises you can emulate the functionality using delay like so:

(-> (p/delay 1000 "foobar")
    (p/then (fn [v]
              (println "Received:" v))))

;; After 1 second it will print the message
;; to the console: "Received: foobar"

The promise library offers the ability to add a timeout to async operations thanks to the timeout function:

(-> (some-async-task)
    (p/timeout 200)
    (p/then #(println "Task finished" %))
    (p/catch #(println "Timeout" %)))

In case the async task is slow, in the example more that 200ms, the promise will be rejected with timeout error and successfully captured with the catch handler.

Scheduling Tasks

Additionally to the promise abstraction, this library also comes with lightweight abstraction for scheduling task to be executed at some time in future:

Example using a schedule function.
(p/schedule 1000 (fn []
                   (println "hello world")))

This example shows you how you can schedule a function call to be executed 1 second in the future. It works in the same way for both plaforms (clj and cljs).

The tasks can be cancelled using its return value:

(def task (p/schedule 1000 #(do-stuff)))

(p/cancel! task)

Developers Guide

Contributing

Unlike Clojure and other Clojure contrib libs, does not have many restrictions for contributions. Just open a issue or pull request.

Get the Code

promesa is open source and can be found on github.

You can clone the public repository with this command:

git clone https://github.com/funcool/promesa

Run tests

To run the tests execute the following:

For the JVM platform:

lein test

And for JS platform:

./scripts/build
node out/tests.js

You will need to have nodejs installed on your system.

License

promesa is licensed under BSD (2-Clause) license:

Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Can you improve this documentation? These fine people already did:
Andrey Antukh, Alejandro Gómez, JarrodCTaylor & Igor Bondarenko
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close