@dancrumb/fpish
Version:
FP-friendly classes for Typescript
139 lines (138 loc) • 3.9 kB
JavaScript
import { Either } from "./Either.js";
import { Optional } from "./Optional.js";
import { RemoteDataStatus } from "./RemoteDataStatus.js";
/**
* This class represents data from a remote source that takes time to load.
*
* This data can be a single value or an array of values. Since there's no way to
* infer the shape of the data, the caller is expected to know and to make the
* appropriate calls
*
*
* @template D A type representing the shape of data that is being requested
* @template E A type representing the shape of errors that can be returned
*/
export class AsyncDatum {
status = RemoteDataStatus.NotAsked;
#internal;
constructor({ status, data, error, }) {
this.status = status;
if (data) {
this.#internal = Either.right(data);
}
else if (error) {
this.#internal = Either.left(error);
}
else {
this.#internal = Either.right(Optional.empty());
}
}
/**
* Create an instance of this type that indicates that the request for async data
* has not yet been made
*/
static notAsked() {
return new AsyncDatum({
status: RemoteDataStatus.NotAsked,
});
}
/**
* Create an instance of this type that indicates that a request is in flight but has not
* yet completed
*/
static loading() {
return new AsyncDatum({
status: RemoteDataStatus.Loading,
});
}
/**
* Create an instance of this type that indicates that some data has been returned by the request.
*
* NB: There is nothing here that asserts that the request is complete. This factory method can
* be called multiple times to indicate loading data in progress.
*
* @param data
*/
static loaded(data) {
return new AsyncDatum({
status: RemoteDataStatus.Succeeded,
data: Object.freeze(data),
});
}
/**
* Create an instance of this type that indicates that the requests has errored.
* @param error
*/
static errored(error) {
return new AsyncDatum({
status: RemoteDataStatus.Failed,
error,
});
}
/**
*
* @returns true if this object currently contains retrievable data, i.e. not an error and not an inflight request
*/
containsData() {
return this.#internal.isRight();
}
/**
* Check the status of the data request
*
* @param status
*/
is(status) {
return this.status === status;
}
/**
* Check whether data has been requested
*/
isAsked() {
return !this.is(RemoteDataStatus.NotAsked);
}
/**
* Check whether data is currently loading
*/
isLoading() {
return this.is(RemoteDataStatus.Loading);
}
/**
* Check whether any data has loaded (or that the request has failed)
*/
isLoaded() {
return this.is(RemoteDataStatus.Succeeded) || this.is(RemoteDataStatus.Failed);
}
/**
* Check whether the data errored out
*/
isErrored() {
return this.is(RemoteDataStatus.Failed);
}
/**
*
*/
value() {
if (this.containsData()) {
return this.#internal.getRight();
}
if (this.is(RemoteDataStatus.Failed)) {
throw this.#internal.getLeft();
}
throw new Error('Trying to access AsyncDatum before it has data');
}
/**
* Get the data as an optional and treat it as a single value
*/
getOptional() {
return this.containsData() ? Optional.of(this.value()) : Optional.empty();
}
/**
* Treats the data as an Optional and returns the internal
* value or the provided value.
*
* @param v
*/
orElse(v) {
return this.getOptional().orElse(v);
}
}