From 3a705d3e28ecf00b5fe99982e420ab6dffd02405 Mon Sep 17 00:00:00 2001 From: Kirill Agalakov Date: Fri, 21 Sep 2018 18:20:08 +0300 Subject: [PATCH] feat: Add progress to RemotePending closes #9 --- src/__tests__/remote-data.spec.ts | 48 +++++++++++++++++++++++++++++ src/remote-data.ts | 51 ++++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/__tests__/remote-data.spec.ts b/src/__tests__/remote-data.spec.ts index 7962b3a..7df5d83 100644 --- a/src/__tests__/remote-data.spec.ts +++ b/src/__tests__/remote-data.spec.ts @@ -13,6 +13,8 @@ import { fromOption, fromEither, fromPredicate, + progress, + fromProgressEvent, } from '../remote-data'; import { identity, compose } from 'fp-ts/lib/function'; import { sequence, traverse } from 'fp-ts/lib/Traversable'; @@ -378,6 +380,41 @@ describe('RemoteData', () => { expect(combine.apply(null, values)).toEqual(failure('bar')); expect(combine.apply(null, values.reverse())).toEqual(failure('bar')); }); + describe('progress', () => { + it('should combine pendings without progress', () => { + const values = [pending, pending]; + expect(combine.apply(null, values)).toBe(pending); + expect(combine.apply(null, values.reverse())).toBe(pending); + }); + it('should combine pending and progress', () => { + const withProgress = progress({ loaded: 1, total: none }); + const values = [pending, withProgress]; + expect(combine.apply(null, values)).toBe(withProgress); + expect(combine.apply(null, values.reverse())).toBe(withProgress); + }); + it('should combine progress without total', () => { + const withProgress = progress({ loaded: 1, total: none }); + const values = [withProgress, withProgress]; + expect(combine.apply(null, values)).toEqual(progress({ loaded: 2, total: none })); + expect(combine.apply(null, values.reverse())).toEqual(progress({ loaded: 2, total: none })); + }); + it('should combine progress without total and progress with total', () => { + const withProgress = progress({ loaded: 1, total: none }); + const withProgressAndTotal = progress({ loaded: 1, total: some(2) }); + const values = [withProgress, withProgressAndTotal]; + expect(combine.apply(null, values)).toEqual(progress({ loaded: 2, total: none })); + expect(combine.apply(null, values.reverse())).toEqual(progress({ loaded: 2, total: none })); + }); + it('should combine progresses with total', () => { + const values = [progress({ loaded: 2, total: some(10) }), progress({ loaded: 2, total: some(30) })]; + const expected = progress({ + loaded: (2 * 10 + 2 * 30) / (40 * 40), + total: some(10 + 30), + }); + expect(combine.apply(null, values)).toEqual(expected); + expect(combine.apply(null, values.reverse())).toEqual(expected); + }); + }); }); describe('fromOption', () => { const error = new Error('foo'); @@ -405,6 +442,17 @@ describe('RemoteData', () => { expect(factory(true)).toEqual(success(true)); }); }); + describe('fromProgressEvent', () => { + const e = new ProgressEvent('test'); + it('lengthComputable === false', () => { + expect(fromProgressEvent({ ...e, loaded: 123 })).toEqual(progress({ loaded: 123, total: none })); + }); + it('lengthComputable === true', () => { + expect(fromProgressEvent({ ...e, loaded: 123, lengthComputable: true, total: 1000 })).toEqual( + progress({ loaded: 123, total: some(1000) }), + ); + }); + }); }); describe('instance methods', () => { describe('getOrElse', () => { diff --git a/src/remote-data.ts b/src/remote-data.ts index 39c3dad..675fafd 100644 --- a/src/remote-data.ts +++ b/src/remote-data.ts @@ -27,6 +27,40 @@ declare module 'fp-ts/lib/HKT' { } } +export type RemoteProgress = { + loaded: number; + total: Option; +}; +const concatPendings = (a: RemotePending, b: RemotePending): RemotePending => { + const noA = a.progress.isNone(); + const noB = b.progress.isNone(); + if (a.progress.isSome() && b.progress.isSome()) { + const progressA = a.progress.value; + const progressB = b.progress.value; + if (progressA.total.isNone() || progressB.total.isNone()) { + 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), + }); + } + if (noA && !noB) { + return b; + } + if (!noA && noB) { + return a; + } + return pending; +}; + export class RemoteInitial { readonly _tag: 'RemoteInitial' = 'RemoteInitial'; // prettier-ignore @@ -892,6 +926,8 @@ export class RemotePending { // prettier-ignore readonly '_L': L; + constructor(readonly progress: Option = none) {} + /** * `alt` short for alternative, takes another `RemoteData`. * If `this` `RemoteData` is a `RemoteSuccess` type then it will be returned. @@ -934,7 +970,12 @@ export class RemotePending { * `failure(new Error('err text')).ap(initial) will return initial.` */ ap(fab: RemoteData>): RemoteData { - return fab.fold(initial, pending as any, () => pending, () => pending); //tslint:disable-line no-use-before-declare + return fab.fold( + initial, + fab.isPending() ? (concatPendings(this, fab as any) as any) : this, + () => this, + () => this, + ); //tslint:disable-line no-use-before-declare } /** @@ -1222,6 +1263,7 @@ const extend = (fla: RemoteData, f: Function1, B export const failure = (error: L): RemoteFailure => new RemoteFailure(error); export const success: (value: A) => RemoteSuccess = of; export const pending: RemotePending = new RemotePending(); +export const progress = (progress: RemoteProgress): RemotePending => new RemotePending(some(progress)); export const initial: RemoteInitial = new RemoteInitial(); //Alternative @@ -1312,6 +1354,13 @@ export function fromPredicate( return a => (predicate(a) ? success(a) : failure(whenFalse(a))); } +export function fromProgressEvent(event: ProgressEvent): RemotePending { + return progress({ + loaded: event.loaded, + total: event.lengthComputable ? some(event.total) : none, + }); +} + //instance export const remoteData: Monad2 & Foldable2 &