Packages

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

Current section

Files

Jump to
future src futureFfi.mjs
Raw

src/futureFfi.mjs

// 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.
import { Completed, Failed, Released, Running } from "./future.mjs";
import { Result, Ok, Error as GleamError } from "./gleam.mjs";
/**
* @typedef {Completed | Failed | Released | Running} Status
* @typedef {Promise<Failed | Result> & { status: Status }} Future
*/
/**
* @function _doAsFuture
* @template T
* @param {function(T): Result | Promise<Result>} executable
* @param {T} argument
* @returns {Future}
*/
export function _doAsFuture(executable, argument) {
const store = { value: null };
let start = null;
store.value = new Promise((resolve, _) => {
start = resolve;
}).then(async (_) => {
try {
let result = executable(argument);
if (result instanceof Promise)
result = await result;
if (result instanceof Failed)
store.value.status = result;
else
store.value.status = new Completed(result);
return result;
} catch (error) {
let ret;
if (error === undefined || error === null)
ret = 'JS Error was Nil';
else if (typeof error === 'string')
ret = error;
else if ('message' in error && typeof (error.message) === 'string')
ret = error.message;
else if (error.toString && typeof error.toString === 'function')
ret = error.toString();
else
ret = JSON.stringify(error);
ret = new Failed(ret);
store.value.status = ret;
return ret;
}
});
store.value.status = new Running();
start();
return store.value;
}
export function start(executable, starting_value) {
return _doAsFuture(executable, starting_value);
}
/**
* @function then
* @param {Future} future
* @param {function(Result): Result | Failed} next
* @returns {Future}
*/
export function then(future, next) {
if (future.status instanceof Released) {
const promise = Promise.resolve(undefined);
promise.status = new Failed("Prior future was released before initialisation");
return promise;
}
return _doAsFuture(async (_) => {
const result = await future;
if (result instanceof Failed)
return new Failed("Prior future failed: " + result.reason);
return next(result);
}, null);
}
/**
* @function fromResult
* @param {Result} result
* @returns {Future}
*/
export function fromResult(result) {
const promise = Promise.resolve(result);
promise.status = new Completed(result);
return promise;
}
/**
* @function fromFailure
* @param {string} reason
* @returns {Future}
*/
export function fromFailure(reason) {
const promise = Promise.resolve(new Failed(reason));
promise.status = new Failed(reason);
return promise;
}
/**
* @function status
* @param {Future} future
* @returns {Status}
*/
export function status(future) {
return future.status;
}
/**
* @function isComplete
* @param {Future} future
* @returns {boolean}
*/
export function isComplete(future) {
return future.status instanceof Completed
|| future.status instanceof Failed
|| future.status instanceof Released;
}
/**
* @function release
* @param {Future} future
* @returns {void}
*/
export function release(future) {
future.status = new Released();
}
/**
* @function addFailureHandler
* @param {Future} future
* @param {function(string): void} handler
* @returns {Future} The provided future for chaining methods
*/
export function addFailureHandler(future, handler) {
future.then((result) => {
if (result instanceof Failed)
handler(result.reason);
});
return future;
}
/**
* @function autoRelease
* @param {Future} future
* @returns {Future}
*/
export function autoRelease(future) {
future.finally(() => release(future));
return future;
}
/**
* @function after
* @param {Future} future
* @param {function(Status): void} executable
* @returns {Future} The provided future for chaining methods
*/
export function after(future, executable) {
(async() => {
if (future.status instanceof Released) {
executable(new Released());
return;
}
const result = await future;
if (result instanceof Failed) {
executable(result);
return;
}
executable(new Completed(result));
})();
return future;
}
/**
* @function createManual
* @returns {[function(Result): void, function(Result): void, function(string): void, Future]}
*/
export function createManual() {
let resolveFunction = null;
let failFunction = null;
const future = new Promise((resolve, reject) => {
resolveFunction = resolve;
failFunction = reject;
}).catch(error => {
return new Failed(error);
});
future.status = new Running();
return [function (any) {
if (!(future.status instanceof Running))
return;
const result = new Ok(any);
future.status = new Completed(result);
resolveFunction(result);
}, function (any) {
if (!(future.status instanceof Running))
return;
const result = new GleamError(any);
future.status = new Completed(result);
resolveFunction(result);
}, function (string) {
if (!(future.status instanceof Running))
return;
if (typeof string !== 'string')
throw new Error("Fail function expected string but got " + typeof string);
const result = new Failed(string);
future.status = result;
failFunction(result);
}, future];
}
/**
* Creates a future that resolves after the provided duration
*
* @function delayOf
* @param {number} duration In milliseconds
* @returns {Future} that resolves after the provided duration
*/
export function delayOf(duration) {
if (duration < 1)
return fromFailure("Delay duration must be a positive integer");
const manual = createManual();
setTimeout(() => manual[0](new Ok(undefined)), duration);
return manual[3];
}