std/async

Async functions give tasks, and await waits for one. This module makes tasks of its own, settled when a timer fires, when other tasks settle, or by hand, and settles each one as soon as its outcome is known:

import "std/async"

async func fetch_all(str[] urls) Result[str[]] do
    Task[str][] pages = []
    for url in urls do
        push(pages, with_timeout(download(url), 5000))
    end
    return Result.OK(await all_of(pages)?)
end
Function Result
resolve(task, value), reject(task, error) settles a task made with Task(), unless it has settled already
on_settled(task, callback) runs callback with the task's Result once it settles: soon, if it has
ready(value) a task that has settled with value
delay(ms, token) a Task[bool] that settles with true after ms milliseconds; with a CancelToken, it fails with kind "cancelled" when the token is cancelled first
and_then(task, f) a task with what f gives for the task's value; an error passes through
recover(task, f) a task with the task's value, or what f gives for its error
all_of(tasks) a task with the values of all the tasks, in their order, or the first error to come
any_of(tasks) a task with the first value to come, or the last error when all of them fail
race(tasks) a task that settles as the first of the tasks to settle does
with_timeout(task, ms) a task that settles as task does, or fails with kind "timeout" after ms milliseconds
cancellable(task, token) a task that settles as task does, or fails with kind "cancelled" when token is cancelled first

A CancelToken says that work a program started is no longer wanted: token.cancel(reason) cancels it once, cancelled and reason say whether and why, and token.on_cancel(callback) runs callback with the reason when it is cancelled, at once if it has been. Cancelling does not stop work by itself: the work looks at the token, as delay and cancellable do.

CancelToken stop = CancelToken()
on_signal("interrupt", func() do
    stop.cancel("interrupted")
end)
match await delay(60000, stop) do
    when OK(_) then
        print("a minute went by")
    when ERROR(e) then
        print(e.message)                // interrupted
end

Coordinating tasks

These help tasks work together without stepping on each other:

Type or function For
Queue[T] values passed from tasks that make them to tasks that use them, in order: q.put(value) and q.take() give tasks; with capacity above 0, put waits while the queue is full, so a producer cannot run ahead of its consumers; q.close() makes waiting and later calls fail with kind "closed", and q.size() says how many values wait
Semaphore(permits) at most permits tasks at once: s.acquire() gives a task that settles once a permit is held, in the order asked, and s.release() gives it back
Event() something that has happened or not: e.wait() settles once e.set() is called, at once after it, and e.clear() makes later waits wait again
debounce(ms, f) a function that runs f once calls to it stop, ms milliseconds after the last
throttle(ms, f) a function that runs f at most once every ms milliseconds: the first call at once, and any during the wait once at its end
rate_limiter(per_second, burst) a RateLimiter, a bucket of burst tokens that refills at per_second: l.take() gives a task that settles once a token is taken, in the order asked, and l.try_take() takes one only if it is free
retry(attempt, options) runs attempt, a function that gives a task, again after each failure as a RetryOptions says, and gives the first value or the last error

RetryOptions has the number of attempts in all (5), the wait after the first failure, first_delay_ms (100), the factor each later wait grows by (2.0), the longest wait, max_delay_ms (10000), the jitter, a fraction by which each wait is made longer or shorter at random (0.2), so that many clients do not try again at the same moment, and retry_if, which says whether an error is worth another try:

RetryOptions options = RetryOptions()
options.retry_if = func(Error e) => e.kind != "not_found"
str page = await retry(func() => download(url), options)?

all_of, any_of, and_then, and recover have these names because all, any, then, and catch are taken by the functions for arrays and by keywords. The other modules have tasks of their own: finished and capture_task for processes, and next_change for files.