Future<Value>
The Future is a replacement for Promise.
Main differences with Promises
- Futures don't handle rejection state, instead leaving it to a contained
Result - Futures have built-in cancellation (and don't reject like the fetch
signalAPI does) - Futures don't "swallow" futures that are returned from
mapandflatMap - Future callbacks run synchronously
Even though we're diverging from Promise, you can await a Future.
Create a Future
import { Future } from "@swan-io/boxed";
// Value
const future = Future.value(1);
// Simple future
const otherFuture = Future.make((resolve) => {
resolve(1);
});
// Future with cancellation effect
const otherFuture = Future.make((resolve) => {
const timeoutId = setTimeout(() => {
resolve(1);
}, 1000);
return () => clearTimeout(timeoutId);
});
Methods
.onResolve(f)
Future<A>.onResolve(func: (value: A) => void): void
Runs f with the future value as argument when available.
Future.value(1).onResolve(console.log);
// Log: 1
.onCancel(f)
Future<A>.onCancel(func: () => void): void
Runs f when the future is cancelled.
future.onCancel(() => {
// do something
});
.map(f)
Future<A>.map<B>(func: (value: A) => B, propagateCancel?: boolean): Future<B>
Takes a Future<A> and returns a new Future<f<A>>
Future.value(3).map((x) => x * 2);
// Future<6>
.flatMap(f)
Future<A>.flatMap<B>(func: (value: A) => Future<B>, propagateCancel?: boolean): Future<B>
Takes a Future<A>, and returns a new future taking the value of the future returned by f(A)
Future.value(3).flatMap((x) => Future.value(x * 2));
// Future<6>
.tap(f)
Future<A>.tap(func: (value: A) => unknown): Future<A>
Runs f with the future value, and returns the original future. Useful for debugging.
Future.value(3).tap(console.log);
// Log: 3
// Future<3>
.toPromise()
Future<A>.toPromise(): Promise<A>
Takes a Future<T> and returns a Promise<T>
Future.value(1).toPromise();
// Promise<1>
Future<Result<Ok, Error>>
We provide some special helpers for Futures containing a Result.
Statics
Future.isFuture(value)
isFuture(value: unknown): boolean
Type guard, checks if the provided value is a future.
Future.isFuture(Future.value(1));
// true
Future.isFuture([]);
// false
Future.all(futures)
all(futures: Array<Future<A>>): Future<Array<A>>
Turns an "array of futures of values" into a "future of array of value".
Future.all([Future.value(1), Future.value(2), Future.value(3)]);
// Future<[1, 2, 3]>
Future.concurrent(futureGetters, options)
all(futures: Array<() => Future<A>>, {concurrency: number}): Future<Array<A>>
Like Future.all with a max concurrency, and in order to control the flow, provided with functions returning futures.
Future.concurrent(
userIds.map((userId) => {
// notice we return a function
return () => getUserById(userId);
}),
{ concurrency: 10 },
);
// Future<[...]>
Future.wait(ms)
wait(ms: number): Future<void>
Helper to create a future that resolves after ms (in milliseconds).
Future.wait(1000).tap(() => console.log("Hey"));
// Logs "Hey" after 1s
Future.allFromDict(futures)
allFromDict(futures: Dict<Future<A>>): Future<Dict<A>>
Turns a "dict of futures of values" into a "future of dict of value".
Future.allFromDict({
a: Future.value(1),
b: Future.value(2),
c: Future.value(3),
});
// Future<{a: 1, b: 2, c: 3}>
Future.fromPromise(promise)
fromPromise<A>(promise: Promise<A>): Future<Result<A, unknown>>
Takes a Promise<T> and returns a Future<Result<T, Error>>
Future.fromPromise(Promise.resolve(1));
// Future<Result.Ok<1>>
Future.fromPromise(Promise.reject(1));
// Future<Result.Error<1>>
Cancellation
Basics
In JavaScript, Promises are not cancellable.
That can be limiting at times, especially when using React's useEffect, that let's you return a cancellation effect in order to prevent unwanted side-effects.
You can return a cleanup effect from the future init function:
const future = Future.make((resolve) => {
const timeoutId = setTimeout(() => {
resolve(1);
}, 1000);
// will run on cancellation
return () => clearTimeout(timeoutId);
});
To cancel a future, call future.cancel().
future.cancel();
You can only cancel a pending future.
Calling cancel on a resolved future is a no-op, meaning the future will keep its resolved state.
A cancelled future will automatically cancel any future created from it (e.g. from .map or .flatMap):
const future = Future.make((resolve) => {
const timeoutId = setTimeout(() => {
resolve(1);
}, 1000);
// will run on cancellation
return () => clearTimeout(timeoutId);
});
const future2 = future.map((x) => x * 2);
future.cancel(); // Both `future` and `future2` are cancelled
Bubbling cancellation
All .map* and .flatMap* methods take an extra parameter called propagateCancel, it enables the returned future cancel to bubble up cancellation to its depedencies:
// disabled by default: cancelling `future2` will not cancel `future`
const future2 = future.map((x) => x * 2);
// optin: cancelling `future2` will cancel `future`
const future2 = future.map((x) => x * 2, true);
This can be useful at call site:
const request = apiCall().map(parse, true);
request.cancel(); // will run the cleanup effect in `apiCall`
Cheatsheet
| Method | Input | Function input | Function output | Returned value |
|---|---|---|---|---|
map | Future(x) | x | y | Future(y) |
flatMap | Future(x) | x | Future(y) | Future(y) |