// 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 }