Packages

A async futures library for gleam supporting erlang and javascript (deno, bun, node)

Current section

Files

Jump to
future src future.gleam
Raw

src/future.gleam

// Copyright 2025 BradBot_1
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
type TaskResult(result, reject) =
Result(result, reject)
type Executable(input, result, reject) =
fn(input) -> TaskResult(result, reject)
pub type Future(input, result, reject)
/// Creates a future from an executable
///
/// > Please note that lifecycle management of this future is managed by YOU!
/// > You MUST release it when you are done with it else have a memory leak! (This comes from how the erlang gc works)
/// > If you want to immediately release the future on completion then use `auto_release`!
@external(erlang, "futureFfi", "start")
@external(javascript, "./futureFfi.mjs", "start")
pub fn new(
executable: Executable(input, result, reject),
starting_value: input,
) -> Future(input, result, reject)
/// Creates a future from a result
///
/// This future will never have the status of running or failed
///
/// > Just as with `new` you own the lifecycle of this future and must release it when you are done with it
@external(erlang, "futureFfi", "start")
@external(javascript, "./futureFfi.mjs", "fromResult")
pub fn from_result(
result: TaskResult(result, reject),
) -> Future(input, result, reject)
@target(javascript)
@external(javascript, "./futureFfi.mjs", "fromFailure")
pub fn from_failure(reason: String) -> Future(input, result, reject)
@target(erlang)
pub fn from_failure(reason: String) -> Future(input, result, reject) {
erlang_start(Failed(reason))
}
@target(erlang)
@external(erlang, "futureFfi", "start")
fn erlang_start(status: Status(result, reject)) -> Future(input, result, reject)
/// Chains a future with another future
///
/// If the first future fails, all chained futures will fail with the same reason with a prefix of "Prior future failed: "
///
/// If the first future is released BEFORE calling then, the second future will fail with the reason "Prior future was released before initialisation"
/// If the first future is released AFTER calling then, the second future will operate as expected and still recieve the result of the first future
@external(erlang, "futureFfi", "start")
@external(javascript, "./futureFfi.mjs", "then")
pub fn then(
future: Future(input, result, reject),
next: Executable(Result(result, reject), result2, reject2),
) -> Future(Result(result, reject), result2, reject2)
/// If the future has completed, IE a result is available or failed or was released
@external(erlang, "futureFfi", "is_complete")
@external(javascript, "./futureFfi.mjs", "isComplete")
pub fn is_complete(future: Future(input, result, reject)) -> Bool
/// The possible statuses of a future
pub type Status(result, reject) {
/// The future has completed and returned a result
Completed(TaskResult(result, reject))
/// The future is still executing code
Running
/// The future has failed for the provided reason
Failed(reason: String)
/// The future has been released and the state it once held is no longer available
Released
}
/// Returns the status of the future
@external(erlang, "futureFfi", "status")
@external(javascript, "./futureFfi.mjs", "status")
pub fn status(future: Future(input, result, reject)) -> Status(result, reject)
/// Releases the future, allowing it to be garbage collected
///
/// If the future is still running only the storage for the state will be released, the execution will continue and result will still be made available for chained futures
/// If the future has completed it will be marked for garbage collection and the state will be made unavailable
///
/// You should assume that any future not released will never be garbage collected
@external(erlang, "futureFfi", "release")
@external(javascript, "./futureFfi.mjs", "release")
pub fn release(future: Future(input, result, reject)) -> Nil
/// Adds a handler to the future that will be called if the future fails
///
/// The handler will be called with the reason for the failure
///
/// If the future is released (before or after) this will do nothing
///
/// This handler does not have to be released as it's lifecycle is self contained
@external(erlang, "futureFfi", "create_failure_handler")
@external(javascript, "./futureFfi.mjs", "addFailureHandler")
pub fn add_failure_handler(
future: Future(input, result, reject),
handler: fn(String) -> Nil,
) -> Future(input, result, reject)
@target(erlang)
/// Blocks execution until the future is complete
///
/// If the future is already complete this will return immediately
///
/// > This function is only available in the erlang target!
@external(erlang, "futureFfi", "wait")
pub fn wait(future: Future(input, result, reject)) -> Nil
/// Registers the given exectuable to be invoked once the future is complete
///
/// This just registers:
/// - A new future via `then`
/// - A handler via `add_failure_handler`
@external(erlang, "futureFfi", "once_complete")
@external(javascript, "./futureFfi.mjs", "after")
pub fn after(
future: Future(input, result, reject),
executable: fn(Status(result, reject)) -> a,
) -> Future(input, result, reject) {
case status(future) {
Running -> {
{
use message <- add_failure_handler(future)
executable(Failed(message))
Nil
}
{
use value <- then(future)
executable(Completed(value))
Ok(Nil)
}
|> auto_release
Nil
}
other_status -> {
new(
fn(status) {
executable(status)
Ok(Nil)
},
other_status,
)
|> auto_release
Nil
}
}
future
}
/// Automatically releases the future when it's complete
///
/// The future returned is the one provided! You no longer need to call `release` on it (but doing so will do nothing)
@external(erlang, "futureFfi", "auto_release")
@external(javascript, "./futureFfi.mjs", "autoRelease")
pub fn auto_release(
future: Future(input, result, reject),
) -> Future(input, result, reject) {
{
use _ <- add_failure_handler(future)
release(future)
Nil
}
{
use _ <- then(future)
release(future)
Ok(Nil)
}
|> release
future
}