import { constFalse, FunctionN, Lazy, Predicate, identity, constant, Endomorphism } from 'fp-ts/lib/function'; import { Monad2 } from 'fp-ts/lib/Monad'; import { Foldable2 } from 'fp-ts/lib/Foldable'; import { Alt2 } from 'fp-ts/lib/Alt'; import { Extend2 } from 'fp-ts/lib/Extend'; import { Traversable2 } from 'fp-ts/lib/Traversable'; import { Bifunctor2 } from 'fp-ts/lib/Bifunctor'; import { isNone, isSome, none, Option, some, fold as foldO, getShow as getShowOption } from 'fp-ts/lib/Option'; import { Either, left, right, fold as foldEither } from 'fp-ts/lib/Either'; import { Eq } from 'fp-ts/lib/Eq'; import { array } from 'fp-ts/lib/Array'; import { HKT } from 'fp-ts/lib/HKT'; import { Applicative } from 'fp-ts/lib/Applicative'; import { Alternative2 } from 'fp-ts/lib/Alternative'; import { Ord } from 'fp-ts/lib/Ord'; import { sign } from 'fp-ts/lib/Ordering'; import { Semigroup } from 'fp-ts/lib/Semigroup'; import { Monoid } from 'fp-ts/lib/Monoid'; import { pipe, pipeable } from 'fp-ts/lib/pipeable'; import { Show, showNumber } from 'fp-ts/lib/Show'; export const URI = 'RemoteData'; export type URI = typeof URI; declare module 'fp-ts/lib/HKT' { interface URItoKind2 { RemoteData: RemoteData; } } export type RemoteProgress = { readonly loaded: number; readonly total: Option; }; export type RemoteInitial = { readonly _tag: 'RemoteInitial'; }; export type RemotePending = { readonly _tag: 'RemotePending'; readonly progress: Option; }; export type RemoteFailure = { readonly _tag: 'RemoteFailure'; readonly error: E; }; export type RemoteSuccess = { readonly _tag: 'RemoteSuccess'; readonly value: A; }; /** * Represents a value of one of four possible types (a disjoint union) * * An instance of {@link RemoteData} is either an instance of {@link RemoteInitial}, {@link RemotePending}, {@link RemoteFailure} or {@link RemoteSuccess} * * A common use of {@link RemoteData} is as an alternative to `Either` or `Option` supporting initial and pending states (fits best with [RXJS]{@link https://github.com/ReactiveX/rxjs/}). * * Note: {@link RemoteInitial}, {@link RemotePending} and {@link RemoteFailure} are commonly called "Left" part in jsDoc. * * @see https://medium.com/@gcanti/slaying-a-ui-antipattern-with-flow-5eed0cfb627b * */ export type RemoteData = RemoteInitial | RemotePending | RemoteFailure | RemoteSuccess; //constructors export const failure = (error: E): RemoteData => ({ _tag: 'RemoteFailure', error, }); export const success = (value: A): RemoteData => ({ _tag: 'RemoteSuccess', value, }); export const pending: RemoteData = { _tag: 'RemotePending', progress: none, }; export const progress = (progress: RemoteProgress): RemoteData => ({ _tag: 'RemotePending', progress: some(progress), }); export const initial: RemoteData = { _tag: 'RemoteInitial', }; //filters /** * Returns true only if {@link RemoteData} is {@link RemoteFailure} */ export const isFailure = (data: RemoteData): data is RemoteFailure => data._tag === 'RemoteFailure'; /** * Returns true only if {@link RemoteData} is {@link RemoteSuccess} */ export const isSuccess = (data: RemoteData): data is RemoteSuccess => data._tag === 'RemoteSuccess'; /** * Returns true only if {@link RemoteData} is {@link RemotePending} */ export const isPending = (data: RemoteData): data is RemotePending => data._tag === 'RemotePending'; /** * Returns true only if {@link RemoteData} is {@link RemoteInitial} */ export const isInitial = (data: RemoteData): data is RemoteInitial => data._tag === 'RemoteInitial'; /** * Takes a default value as an argument. * If this {@link RemoteData} is "Left" part it will return default value. * If this {@link RemoteData} is {@link RemoteSuccess} it will return it's value ("wrapped" value, not default value) * * Note: Default value should be the same type as {@link RemoteData} (internal) value, if you want to pass different type as default, use {@link fold}. * * @example * getOrElse(() => 999)(some(1)) // 1 * getOrElseValue(() => 999)(initial) // 999 */ export const getOrElse = (f: Lazy) => (ma: RemoteData): A => (isSuccess(ma) ? ma.value : f()); /** * Needed for "unwrap" value from {@link RemoteData} "container". * It applies a function to each case in the data structure. * * @example * const onInitial = () => "it's initial" * const onPending = () => "it's pending" * const onFailure = (err) => "it's failure" * const onSuccess = (data) => `${data + 1}` * const f = fold(onInitial, onPending, onFailure, onSuccess) * * f(initial) // "it's initial" * f(pending) // "it's pending" * f(failure(new Error('error text'))) // "it's failure" * f(success(21)) // '22' */ export const fold = ( onInitial: () => B, onPending: (progress: Option) => B, onFailure: (error: E) => B, onSuccess: (value: A) => B, ) => (ma: RemoteData): B => { switch (ma._tag) { case 'RemoteInitial': { return onInitial(); } case 'RemotePending': { return onPending(ma.progress); } case 'RemoteFailure': { return onFailure(ma.error); } case 'RemoteSuccess': { return onSuccess(ma.value); } } }; /** * A more concise way to "unwrap" values from {@link RemoteData} "container". * It uses fold in its implementation, collapsing `onInitial` and `onPending` on the `onNone` handler. * When fold's `onInitial` returns, `onNode` is called with `none`. * * @example * const onNone = (progressOption) => "no data to show" * const onFailure = (err) => "sorry, the request failed" * const onSuccess = (data) => `result is: ${data + 1}` * const f = fold(onInitial, onPending, onFailure, onSuccess) * * f(initial) // "no data to show" * f(pending) // "no data to show" * f(failure(new Error('error text'))) // "sorry, the request failed" * f(success(21)) // "result is: 22" */ export const fold3 = ( onNone: (progress: Option) => R, onFailure: (e: E) => R, onSuccess: (a: A) => R, ): ((fa: RemoteData) => R) => fold(() => onNone(none), onNone, onFailure, onSuccess); /** * One more way to fold (unwrap) value from {@link RemoteData}. * `Left` part will return `null`. * {@link RemoteSuccess} will return value. * * For example: * * `success(2).toNullable() will return 2` * * `initial.toNullable() will return null` * * `pending.toNullable() will return null` * * `failure(new Error('error text)).toNullable() will return null` * */ export const toNullable = (ma: RemoteData): A | null => (isSuccess(ma) ? ma.value : null); export const toUndefined = (ma: RemoteData): A | undefined => (isSuccess(ma) ? ma.value : undefined); export function fromOption(option: Option, error: Lazy): RemoteData { if (isNone(option)) { return failure(error()); } else { return success(option.value); } } /** * Convert {@link RemoteData} to {@link Option} * `Left` part will be converted to {@link None}. * {@link RemoteSuccess} will be converted to {@link Some}. * * @example * toOption(success(2)) // some(2) * toOption(initial) // none * toOption(pending) // none * toOption(failure(new Error('error text'))) // none */ export function toOption(data: RemoteData): Option { return data._tag === 'RemoteSuccess' ? some(data.value) : none; } /** * Creates {@link RemoteData} from {@link Either} */ export const fromEither: (ea: Either) => RemoteData = foldEither(failure, success as any); /** * Convert {@link RemoteData} to `Either`. * `Left` part will be converted to `Left`. * Since {@link RemoteInitial} and {@link RemotePending} do not have `L` values, * you must provide a value of type `L` that will be used to construct * the `Left` for those two cases. * {@link RemoteSuccess} will be converted to `Right`. * * @example: * const f = toEither( * () => new Error('Data not fetched'), * () => new Error('Data is fetching') * ) * f(success(2)) // right(2) * f(initial) // right(Error('Data not fetched')) * f(pending) // right(Error('Data is fetching')) * f(failure(new Error('error text'))) // right(Error('error text')) */ export function toEither(onInitial: () => E, onPending: () => E): (data: RemoteData) => Either { return data => pipe( data, fold(() => left(onInitial()), () => left(onPending()), left, right), ); } export function fromPredicate( predicate: Predicate, whenFalse: FunctionN<[A], L>, ): FunctionN<[A], RemoteData> { return a => (predicate(a) ? success(a) : failure(whenFalse(a))); } /** * Create {@link RemoteData} from {@link ProgressEvent} * @param event */ export function fromProgressEvent(event: ProgressEvent): RemoteData { return progress({ loaded: event.loaded, total: event.lengthComputable ? some(event.total) : none, }); } /** * Compare values and returns `true` if they are identical, otherwise returns `false`. * `Left` part will return `false`. * {@link RemoteSuccess} will call {@link Eq.equals}. * * If you want to compare {@link RemoteData}'s values better use {@link getEq} or {@link getOrd} helpers. * */ export function elem(E: Eq): (a: A, fa: RemoteData) => boolean { return (a, fa) => fa._tag === 'RemoteSuccess' && E.equals(a, fa.value); } /** * Takes a predicate and apply it to {@link RemoteSuccess} value. * `Left` part will return `false`. */ export function exists(p: Predicate): (fa: RemoteData) => boolean { return fa => fa._tag === 'RemoteSuccess' && p(fa.value); } /** * Maps this RemoteFailure error into RemoteSuccess if passed function `f` return {@link Some} value, otherwise returns self */ export function recover(f: (error: E) => Option): (fa: RemoteData) => RemoteData { const r: Endomorphism> = recoverMap(f, identity); return fa => (fa._tag === 'RemoteFailure' ? r(fa) : fa); } /** * Recovers {@link RemoteFailure} also mapping {@link RemoteSuccess} case * @see {@link recover} */ export function recoverMap( f: (error: E) => Option, g: (value: A) => B, ): (fa: RemoteData) => RemoteData { return fa => { switch (fa._tag) { case 'RemoteInitial': { return fa; } case 'RemotePending': { return fa; } case 'RemoteFailure': { const b = f(fa.error); return b._tag === 'Some' ? success(b.value) : fa; } case 'RemoteSuccess': { return success(g(fa.value)); } } }; } const concatPendings = (a: RemotePending, b: RemotePending): RemoteData => { if (isSome(a.progress) && isSome(b.progress)) { const progressA = a.progress.value; const progressB = b.progress.value; if (isNone(progressA.total) || isNone(progressB.total)) { return progress({ loaded: progressA.loaded + progressB.loaded, total: none, }); } const totalA = progressA.total.value; const totalB = progressB.total.value; const total = totalA + totalB; const loaded = (progressA.loaded * totalA + progressB.loaded * totalB) / (total * total); return progress({ loaded, total: some(total), }); } const noA = isNone(a.progress); const noB = isNone(b.progress); if (noA && !noB) { return b; } if (!noA && noB) { return a; } return pending; }; //instance export const remoteData: Monad2 & Foldable2 & Traversable2 & Bifunctor2 & Alt2 & Extend2 & Alternative2 = { //HKT URI, //Monad of: (value: A): RemoteData => success(value), ap: (fab: RemoteData>, fa: RemoteData): RemoteData => { switch (fa._tag) { case 'RemoteInitial': { return isFailure(fab) ? fab : initial; } case 'RemotePending': { return isPending(fab) ? concatPendings(fa, fab) : isSuccess(fab) ? fa : fab; } case 'RemoteFailure': { return isFailure(fab) ? fab : fa; } case 'RemoteSuccess': { return isSuccess(fab) ? success(fab.value(fa.value)) : fab; } } }, map: (fa: RemoteData, f: FunctionN<[A], B>): RemoteData => isSuccess(fa) ? success(f(fa.value)) : fa, chain: (fa: RemoteData, f: FunctionN<[A], RemoteData>): RemoteData => isSuccess(fa) ? f(fa.value) : fa, //Foldable reduce: (fa: RemoteData, b: B, f: FunctionN<[B, A], B>): B => pipe( fa, fold(() => b, () => b, () => b, a => f(b, a)), ), reduceRight: (fa: RemoteData, b: B, f: (a: A, b: B) => B): B => (isSuccess(fa) ? f(fa.value, b) : b), foldMap: (M: Monoid) => (fa: RemoteData, f: (a: A) => M): M => isSuccess(fa) ? f(fa.value) : M.empty, //Traversable traverse: (F: Applicative) => ( ta: RemoteData, f: (a: A) => HKT, ): HKT> => { if (isSuccess(ta)) { return F.map(f(ta.value), a => remoteData.of(a)); } else { return F.of(ta); } }, sequence: (F: Applicative) => (ta: RemoteData>): HKT> => remoteData.traverse(F)(ta, identity), //Bifunctor bimap: (fla: RemoteData, f: (u: L) => V, g: (a: A) => B): RemoteData => pipe( fla, fold>( () => initial, foldO(() => pending, progress), e => failure(f(e)), a => success(g(a)), ), ), mapLeft: (fla: RemoteData, f: (u: L) => V): RemoteData => fold>( () => initial, foldO(() => pending, progress), e => failure(f(e)), () => fla as any, )(fla), //Alt alt: (fx: RemoteData, fy: () => RemoteData): RemoteData => fold(fy, fy, fy, () => fx)(fx), //Alternative zero: (): RemoteData => initial, //Extend extend: (fla: RemoteData, f: FunctionN<[RemoteData], B>): RemoteData => pipe( fla, fold>( () => initial, foldO(() => pending, progress), () => fla as any, () => success(f(fla)), ), ), }; //Eq export const getEq = (EE: Eq, EA: Eq): Eq> => { return { equals: (x, y) => pipe( x, fold( () => isInitial(y), () => isPending(y), xError => pipe( y, fold(constFalse, constFalse, yError => EE.equals(xError, yError), constFalse), ), ax => pipe( y, fold(constFalse, constFalse, constFalse, ay => EA.equals(ax, ay)), ), ), ), }; }; //Ord const constLt = constant(-1); const constEq = constant(0); const constGt = constant(1); export const getOrd = (OE: Ord, OA: Ord): Ord> => { return { ...getEq(OE, OA), compare: (x, y) => sign( pipe( x, fold( () => pipe( y, fold(constEq, constLt, constLt, constLt), ), () => pipe( y, fold(constGt, constEq, constLt, constLt), ), xError => pipe( y, fold(constGt, constGt, yError => OE.compare(xError, yError), constLt), ), xValue => pipe( y, fold(constGt, constGt, constGt, yValue => OA.compare(xValue, yValue)), ), ), ), ), }; }; //Semigroup export const getSemigroup = (SE: Semigroup, SA: Semigroup): Semigroup> => { return { concat: (x, y) => { const constX = constant(x); const constY = constant(y); return pipe( x, fold( () => pipe( y, fold(constY, constY, constY, constY), ), () => pipe( y, fold(constX, () => concatPendings(x as RemotePending, y as RemotePending), constY, constY), ), xError => pipe( y, fold(constX, constX, yError => failure(SE.concat(xError, yError)), () => y), ), xValue => pipe( y, fold(constX, constX, () => x, yValue => success(SA.concat(xValue, yValue))), ), ), ); }, }; }; //Monoid export const getMonoid = (SL: Semigroup, SA: Semigroup): Monoid> => { return { ...getSemigroup(SL, SA), empty: initial, }; }; const showOptionNumber = getShowOption(showNumber); //Show export const getShow = (SE: Show, SA: Show): Show> => ({ show: fold( () => 'initial', foldO( () => 'pending', progress => `progress({ loaded: ${showNumber.show(progress.loaded)}, total: ${showOptionNumber.show( progress.total, )} })`, ), e => `failure(${SE.show(e)})`, a => `success(${SA.show(a)})`, ), }); const { alt, ap, apFirst, apSecond, bimap, chain, chainFirst, duplicate, extend, flatten, foldMap, map, mapLeft, reduce, reduceRight, } = pipeable(remoteData); export { alt, ap, apFirst, apSecond, bimap, chain, chainFirst, duplicate, extend, flatten, foldMap, map, mapLeft, reduce, reduceRight, }; export function combine(a: RemoteData): RemoteData; export function combine(a: RemoteData, b: RemoteData): RemoteData; export function combine( a: RemoteData, b: RemoteData, c: RemoteData, ): RemoteData; export function combine( a: RemoteData, b: RemoteData, c: RemoteData, d: RemoteData, ): RemoteData; export function combine( a: RemoteData, b: RemoteData, c: RemoteData, d: RemoteData, e: RemoteData, ): RemoteData; export function combine( a: RemoteData, b: RemoteData, c: RemoteData, d: RemoteData, e: RemoteData, f: RemoteData, ): RemoteData; export function combine(...list: RemoteData[]): RemoteData { if (list.length === 0) { return remoteData.of([]); } return array.sequence(remoteData)(list); }