UNPKG

@dancrumb/fpish

Version:

FP-friendly classes for Typescript

139 lines (138 loc) 3.9 kB
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); } }