@theateros/futur
v0.0.1
Published
<p align="center"> <img src="../../.etc/assets/futur-logo.webp" alt="Theater OS - Foundations - Futur"> </p>
Readme
Theater OS - Futur
A lazy, type-safe asynchronous computation wrapper for TypeScript that combines the power of Promises with Result-based error handling, providing deferred execution and built-in cancellation support.
Why Futur?
JavaScript Promises are eager - they start executing immediately upon creation. While this works for many use cases, it has limitations:
- Eager execution: Promises run immediately, making it hard to control when async operations start
- No type-safe errors: Rejected promises lose type information about the error
- No built-in cancellation: AbortController must be managed separately
- Exception-based errors: You need try-catch blocks to handle rejections
Futur addresses these issues by providing:
- Lazy execution: The async operation only runs when you
awaitthe Futur - Type-safe errors: Returns
Result<T, E>with typed success and error values - Built-in cancellation: Every Futur runner receives a special FuturAbortion instance for cancellation control
- No exceptions: Errors are captured as
Result.errvalues, not thrown - Promise interoperability: Works with
async/await,Promise.all,Promise.race, etc.
Installation
npm install @theateros/futurGetting Started
Basic Usage
import { Futur } from "@theateros/futur";
import { Result } from "@theateros/result";
// Create a Futur - it won't run until awaited
const futur = Futur.of<string, Error>(({ resolve }) => {
resolve("Hello, World!");
});
// Run the Futur and get a Result
const result = await futur;
if (Result.isOk(result)) {
console.log(result.value); // "Hello, World!"
}Creating Futurs
Use Futur.of to create a Futur from a runner function:
import { Futur } from "@theateros/futur";
import { Result } from "@theateros/result";
// Success case
const successFutur = Futur.of<number, string>(({ resolve }) => {
resolve(42);
});
// Error case
const errorFutur = Futur.of<number, string>(({ reject }) => {
reject("Something went wrong");
});
// Async operations
const asyncFutur = Futur.of<string, Error>(({ resolve }) => {
setTimeout(() => resolve("Delayed result"), 1000);
});
// All results are type-safe
const result = await successFutur;
if (Result.isOk(result)) {
console.log(result.value); // 42
}
const errorResult = await errorFutur;
if (Result.isErr(errorResult)) {
console.log(errorResult.error); // "Something went wrong"
}Wrapping Existing Promises
Use Futur.ofPromise to wrap existing Promise-returning functions:
import { Futur } from "@theateros/futur";
import { Result } from "@theateros/result";
// Let's take some standard async operation
async function stdFetchUser(id: string): Record<string, string> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return data as Record<string, string>;
}
// Wrap a fetch call
const fetchUser = (id: string) => Futur.ofPromise(() => stdFetchUser(id));
const result = await fetchUser(1);
if (Result.isOk(result)) {
console.log("User:", result.value);
} else {
console.log("Error:", result.error);
}Error Transformation
Transform errors using the optional catcher parameter:
import { Futur } from "@theateros/futur";
import { Result } from "@theateros/result";
interface ApiError {
code: string;
message: string;
}
const fetchData = Futur.ofPromise(
() => fetch("/api/data").then((res) => res.json()),
(error) =>
({
code: "FETCH_ERROR",
message: String(error),
}) as ApiError,
);
const result = await fetchData;
if (Result.isErr(result)) {
// error is typed as ApiError
console.log(`Error ${result.error.code}: ${result.error.message}`);
}Cancellation
Every Futur has built-in cancellation support. When aborted, the Futur rejects with an AbortedFailure.
Aborting from Within the Runner
The runner receives an abortion object in its payload, which provides access to cancellation methods and the abort controller:
import { Futur } from "@theateros/futur";
import { Result } from "@theateros/result";
const fetchWithAbort = Futur.of<Response, Error>(({ resolve, reject, abortion }) => {
// Use the provided abort controller with fetch
fetch("/api/data", { signal: abortion.controller.signal }).then(resolve).catch(reject);
});
const result = await fetchWithAbort;You can also abort directly from within the runner:
import { Futur } from "@theateros/futur";
import { Result } from "@theateros/result";
const conditionalFutur = Futur.of<string, never>(({ resolve, abortion }) => {
// Check some condition and abort if needed
if (someCondition) {
abortion.abort();
return;
}
resolve("success");
});The abortion object provides:
abort(): Abort the Futur operationisAborted(): Check if the Futur has been abortedonAbort(callback): Register a callback for abort eventscontroller: TheAbortControllerinstance for use with fetch, streams, etc.
Aborting from Outside
Use the abort() method to cancel a Futur from outside:
import { Futur, AbortedFailure } from "@theateros/futur";
import { Result } from "@theateros/result";
import { Failure } from "@theateros/failure";
const longRunningTask = Futur.of<string, never>(({ resolve }) => {
setTimeout(() => resolve("completed"), 10000);
});
// Cancel after 1 second
setTimeout(() => {
longRunningTask.abort();
}, 1000);
const result = await longRunningTask;
if (Result.isErr(result)) {
if (Failure.isNamed(result.error, "AbortedFailure")) {
console.log("Task was cancelled:", result.error.message);
// "Task was cancelled: Futur has been aborted"
}
}Checking Abort Status
Use the aborted getter to check if a Futur has been aborted:
import { Futur } from "@theateros/futur";
const futur = Futur.of<string, never>(({ resolve }) => {
setTimeout(() => resolve("done"), 1000);
});
const promise = futur.then();
// Check if aborted
console.log(futur.aborted); // false
futur.abort();
// Note: aborted may be false after cleanup, check the result instead
const result = await promise;Handling AbortedFailure
When a Futur is aborted, it rejects with an AbortedFailure:
import { Futur, AbortedFailure } from "@theateros/futur";
import { Result } from "@theateros/result";
import { Failure } from "@theateros/failure";
const futur = Futur.of<string, Error>(({ resolve, abortion }) => {
// Simulate cancellation
abortion.abort();
});
const result = await futur;
if (Result.isErr(result)) {
// Check if it was aborted
if (result.error instanceof AbortedFailure) {
console.log("Operation was aborted");
}
// Or use Failure.isNamed
if (Failure.isNamed(result.error, "AbortedFailure")) {
console.log("Operation was aborted");
}
}Re-running After Abort
A Futur creates a fresh execution context on each run, so you can re-run a Futur after it was aborted:
import { Futur, AbortedFailure } from "@theateros/futur";
import { Result } from "@theateros/result";
let attempt = 0;
const retryableFutur = Futur.of<string, never>(({ resolve, abortion }) => {
attempt++;
if (attempt === 1) {
abortion.abort(); // Abort first attempt
} else {
resolve(`Success on attempt ${attempt}`);
}
});
// First run - aborted
const result1 = await retryableFutur;
console.log(Result.isErr(result1)); // true
// Second run - succeeds with fresh execution context
const result2 = await retryableFutur;
if (Result.isOk(result2)) {
console.log(result2.value); // "Success on attempt 2"
}Listening to Abort Events
Use onAbort() to register callbacks that will be called when the Futur is aborted. You can use this from within the runner or from outside:
import { Futur } from "@theateros/futur";
// From within the runner
const futur = Futur.of<string, never>(({ resolve, abortion }) => {
// Register abort callback from within the runner
abortion.onAbort(() => {
console.log("Futur was aborted from within!");
});
setTimeout(() => resolve("done"), 1000);
});
// Or from outside
const promise = futur.then();
// Register abort callback from outside
const removeCallback = futur.onAbort(() => {
console.log("Futur was aborted from outside!");
});
// Abort the Futur
futur.abort();
// Remove the callback if needed
removeCallback();
await promise;Deferred Execution
Futurs are lazy - they only execute when awaited:
import { Futur } from "@theateros/futur";
// This does NOT start the operation
const futur = Futur.of<string, never>(({ resolve }) => {
console.log("Running!");
resolve("done");
});
console.log("Futur created");
// This starts the operation
const result = await futur;
// Output:
// "Futur created"
// "Running!"Promise Interoperability
Futurs work seamlessly with Promise utilities:
import { Futur } from "@theateros/futur";
import { Result } from "@theateros/result";
const futur1 = Futur.of<number, never>(({ resolve }) => resolve(1));
const futur2 = Futur.of<number, never>(({ resolve }) => resolve(2));
const futur3 = Futur.of<number, never>(({ resolve }) => resolve(3));
// Use with Promise.all
const results = await Promise.all([futur1, futur2, futur3]);
// results is Result<number, never>[]
// Use with Promise.race
const slowFutur = Futur.of<string, never>(({ resolve }) => {
setTimeout(() => resolve("slow"), 1000);
});
const fastFutur = Futur.of<string, never>(({ resolve }) => {
resolve("fast");
});
const winner = await Promise.race([slowFutur, fastFutur]);
// winner.value === 'fast'Transforming Results
Use the then callback to transform results:
import { Futur } from "@theateros/futur";
import { Result } from "@theateros/result";
const futur = Futur.of<number, string>(({ resolve }) => {
resolve(21);
});
// Transform the result
const doubled = await futur.then((result) => {
if (Result.isOk(result)) {
return result.value * 2;
}
return 0;
});
console.log(doubled); // 42API Reference
Futur Class
The main class that implements PromiseLike<Result<T, E | AbortedFailure>>.
Type Parameters
T: The success value typeE: The error value type
Static Methods
Futur.of<T, E>(runner: FuturRunner): Futur<T, E>Creates a new Futur from a runner function. The runner receives a payload with
resolve,reject, andabortion.const futur = Futur.of<string, Error>(({ resolve, reject, abortion }) => { // Your async logic here resolve("success"); // or: reject(new Error('failure')) // Use abortion.abort() to cancel from within the runner // Use abortion.controller.signal with fetch, streams, etc. });Futur.ofPromise<P, E>(launcher: () => P, catcher?: (error: unknown) => E): Futur<Awaited<P>, E>Creates a new Futur from a Promise-returning function. Optionally transform errors with the
catcherparameter.const futur = Futur.ofPromise( () => fetch("/api/data"), (error) => ({ code: "ERROR", message: String(error) }), );
Instance Methods
then<TResult1>(onfulfilled?): Promise<TResult1>Runs the Futur and returns a Promise. Called automatically when using
await.abort(): voidAborts all active executions of the Futur. When aborted, the Futur rejects with an
AbortedFailure.const futur = Futur.of<string, never>(({ resolve }) => { setTimeout(() => resolve("done"), 5000); }); // Cancel after 1 second setTimeout(() => futur.abort(), 1000); const result = await futur; // Result.err(AbortedFailure)get aborted: booleanReturns
trueif any active execution of the Futur has been aborted,falseotherwise.const futur = Futur.of<string, never>(({ resolve }) => { setTimeout(() => resolve("done"), 1000); }); const promise = futur.then(); console.log(futur.aborted); // false futur.abort(); // Note: aborted may be false after cleanup, check the result insteadonAbort(callback: () => void): () => voidRegisters a callback that will be called when the Futur is aborted. Returns a function to remove the callback.
const futur = Futur.of<string, never>(({ resolve }) => { setTimeout(() => resolve("done"), 1000); }); const promise = futur.then(); const removeCallback = futur.onAbort(() => { console.log("Aborted!"); }); futur.abort(); // Calls the callback // Remove the callback removeCallback(); await promise;
AbortedFailure Class
A named Failure class used when a Futur is aborted. Extends Failure from @theateros/failure.
import { AbortedFailure } from "@theateros/futur";
import { Failure } from "@theateros/failure";
const failure = new AbortedFailure("Operation cancelled");
// Type checking
failure instanceof AbortedFailure; // true
failure instanceof Failure; // true
Failure.isNamed(failure, "AbortedFailure"); // trueTypes
FuturPayload
The payload passed to the runner function:
type FuturPayload = {
resolve: <V>(value: V) => void;
reject: <E>(reason: E) => void;
abortion: FuturAbortion;
};FuturAbortion
The abortion API provided to the runner:
type FuturAbortion = Readonly<{
abort: () => void;
isAborted: () => boolean;
onAbort: (callback: () => void) => () => void;
controller: AbortController;
}>;The abortion property provides:
abort(): Abort the Futur operationisAborted(): Check if the Futur has been abortedonAbort(callback): Register a callback for abort events (returns a function to remove the callback)controller: TheAbortControllerinstance for use with fetch, streams, or other APIs that supportAbortSignal
FuturRunner
The runner function type:
type FuturRunner = (payload: FuturPayload) => void;Best Practices
Use
Futur.offor custom async logic: When you need full control over resolve/reject timingUse
Futur.ofPromisefor wrapping existing Promises: Cleaner syntax for Promise-based APIsAlways handle both success and error cases: Use
Result.isOkandResult.isErrfor type-safe handlingLeverage cancellation: Use
abortion.abort()from within the runner, orfutur.abort()from outside. Useabortion.controller.signalwith fetch, streams, etc.Type your errors: Use the error type parameter to ensure type-safe error handling
Remember Futurs are lazy: The operation won't start until you
awaitthe FuturHandle AbortedFailure: When using cancellation, check for
AbortedFailureto handle cancelled operations gracefullyUse the abort controller: Pass
abortion.controller.signalto fetch, streams, or other APIs that supportAbortSignalfor automatic cancellation
