Skip to content

Commit 4c89823

Browse files
committed
feat: Add progress to RemotePending
closes #9
1 parent eaa8f87 commit 4c89823

File tree

2 files changed

+138
-5
lines changed

2 files changed

+138
-5
lines changed

src/__tests__/remote-data.spec.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import {
1313
fromOption,
1414
fromEither,
1515
fromPredicate,
16+
progress,
17+
fromProgressEvent,
1618
} from '../remote-data';
17-
import { identity, compose } from 'fp-ts/lib/function';
19+
import { identity, compose, Function1 } from 'fp-ts/lib/function';
1820
import { sequence, traverse } from 'fp-ts/lib/Traversable';
1921
import { none, option, some } from 'fp-ts/lib/Option';
2022
import { array } from 'fp-ts/lib/Array';
@@ -30,6 +32,7 @@ describe('RemoteData', () => {
3032
const pendingRD: RemoteData<string, number> = pending;
3133
const successRD: RemoteData<string, number> = success(1);
3234
const failureRD: RemoteData<string, number> = failure('foo');
35+
const progressRD: RemoteData<string, Function1<number, number>> = progress({ loaded: 1, total: none });
3336
describe('Functor', () => {
3437
describe('should map over value', () => {
3538
it('initial', () => {
@@ -124,24 +127,28 @@ describe('RemoteData', () => {
124127
it('initial', () => {
125128
expect(initialRD.ap(initial)).toBe(initialRD);
126129
expect(initialRD.ap(pending)).toBe(initialRD);
130+
expect(initialRD.ap(progressRD)).toBe(initialRD);
127131
expect(initialRD.ap(failedF)).toBe(initialRD);
128132
expect(initialRD.ap(f)).toBe(initialRD);
129133
});
130134
it('pending', () => {
131135
expect(pendingRD.ap(initial)).toBe(initial);
132136
expect(pendingRD.ap(pending)).toBe(pendingRD);
137+
expect(pendingRD.ap(progressRD)).toBe(progressRD);
133138
expect(pendingRD.ap(failedF)).toBe(pendingRD);
134139
expect(pendingRD.ap(f)).toBe(pendingRD);
135140
});
136141
it('failure', () => {
137142
expect(failureRD.ap(initial)).toBe(initial);
138143
expect(failureRD.ap(pending)).toBe(pending);
144+
expect(failureRD.ap(progressRD)).toBe(progressRD);
139145
expect(failureRD.ap(failedF)).toBe(failedF);
140146
expect(failureRD.ap(f)).toBe(failureRD);
141147
});
142148
it('success', () => {
143149
expect(successRD.ap(initial)).toBe(initial);
144150
expect(successRD.ap(pending)).toBe(pending);
151+
expect(successRD.ap(progressRD)).toBe(progressRD);
145152
expect(successRD.ap(failedF)).toBe(failedF);
146153
expect(successRD.ap(f)).toEqual(success(double(1)));
147154
});
@@ -341,6 +348,33 @@ describe('RemoteData', () => {
341348
expect(concat(successRD, failureRD)).toBe(successRD);
342349
expect(concat(success(1), success(1))).toEqual(success(semigroupSum.concat(1, 1)));
343350
});
351+
describe('progress', () => {
352+
it('should concat pendings without progress', () => {
353+
expect(concat(pending, pending)).toEqual(pending);
354+
});
355+
it('should concat pending and progress', () => {
356+
const withProgress: RemoteData<string, number> = progress({ loaded: 1, total: none });
357+
expect(concat(pending, withProgress)).toBe(withProgress);
358+
});
359+
it('should concat progress without total', () => {
360+
const withProgress: RemoteData<string, number> = progress({ loaded: 1, total: none });
361+
expect(concat(withProgress, withProgress)).toEqual(progress({ loaded: 2, total: none }));
362+
});
363+
it('should concat progress without total and progress with total', () => {
364+
const withProgress: RemoteData<string, number> = progress({ loaded: 1, total: none });
365+
const withProgressAndTotal: RemoteData<string, number> = progress({ loaded: 1, total: some(2) });
366+
expect(concat(withProgress, withProgressAndTotal)).toEqual(progress({ loaded: 2, total: none }));
367+
});
368+
it('should combine progresses with total', () => {
369+
const expected = progress({
370+
loaded: (2 * 10 + 2 * 30) / (40 * 40),
371+
total: some(10 + 30),
372+
});
373+
expect(
374+
concat(progress({ loaded: 2, total: some(10) }), progress({ loaded: 2, total: some(30) })),
375+
).toEqual(expected);
376+
});
377+
});
344378
});
345379
});
346380
describe('Monoid', () => {
@@ -378,6 +412,41 @@ describe('RemoteData', () => {
378412
expect(combine.apply(null, values)).toEqual(failure('bar'));
379413
expect(combine.apply(null, values.reverse())).toEqual(failure('bar'));
380414
});
415+
describe('progress', () => {
416+
it('should combine pendings without progress', () => {
417+
const values = [pending, pending];
418+
expect(combine.apply(null, values)).toBe(pending);
419+
expect(combine.apply(null, values.reverse())).toBe(pending);
420+
});
421+
it('should combine pending and progress', () => {
422+
const withProgress = progress({ loaded: 1, total: none });
423+
const values = [pending, withProgress];
424+
expect(combine.apply(null, values)).toBe(withProgress);
425+
expect(combine.apply(null, values.reverse())).toBe(withProgress);
426+
});
427+
it('should combine progress without total', () => {
428+
const withProgress = progress({ loaded: 1, total: none });
429+
const values = [withProgress, withProgress];
430+
expect(combine.apply(null, values)).toEqual(progress({ loaded: 2, total: none }));
431+
expect(combine.apply(null, values.reverse())).toEqual(progress({ loaded: 2, total: none }));
432+
});
433+
it('should combine progress without total and progress with total', () => {
434+
const withProgress = progress({ loaded: 1, total: none });
435+
const withProgressAndTotal = progress({ loaded: 1, total: some(2) });
436+
const values = [withProgress, withProgressAndTotal];
437+
expect(combine.apply(null, values)).toEqual(progress({ loaded: 2, total: none }));
438+
expect(combine.apply(null, values.reverse())).toEqual(progress({ loaded: 2, total: none }));
439+
});
440+
it('should combine progresses with total', () => {
441+
const values = [progress({ loaded: 2, total: some(10) }), progress({ loaded: 2, total: some(30) })];
442+
const expected = progress({
443+
loaded: (2 * 10 + 2 * 30) / (40 * 40),
444+
total: some(10 + 30),
445+
});
446+
expect(combine.apply(null, values)).toEqual(expected);
447+
expect(combine.apply(null, values.reverse())).toEqual(expected);
448+
});
449+
});
381450
});
382451
describe('fromOption', () => {
383452
const error = new Error('foo');
@@ -405,6 +474,17 @@ describe('RemoteData', () => {
405474
expect(factory(true)).toEqual(success(true));
406475
});
407476
});
477+
describe('fromProgressEvent', () => {
478+
const e = new ProgressEvent('test');
479+
it('lengthComputable === false', () => {
480+
expect(fromProgressEvent({ ...e, loaded: 123 })).toEqual(progress({ loaded: 123, total: none }));
481+
});
482+
it('lengthComputable === true', () => {
483+
expect(fromProgressEvent({ ...e, loaded: 123, lengthComputable: true, total: 1000 })).toEqual(
484+
progress({ loaded: 123, total: some(1000) }),
485+
);
486+
});
487+
});
408488
});
409489
describe('instance methods', () => {
410490
describe('getOrElse', () => {

src/remote-data.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,44 @@ declare module 'fp-ts/lib/HKT' {
2727
}
2828
}
2929

30+
export type RemoteProgress = {
31+
loaded: number;
32+
total: Option<number>;
33+
};
34+
const concatPendings = <L, A>(a: RemotePending<L, A>, b: RemotePending<L, A>): RemotePending<L, A> => {
35+
if (a.progress.isSome() && b.progress.isSome()) {
36+
const progressA = a.progress.value;
37+
const progressB = b.progress.value;
38+
if (progressA.total.isNone() || progressB.total.isNone()) {
39+
//tslint:disable no-use-before-declare
40+
return progress({
41+
loaded: progressA.loaded + progressB.loaded,
42+
total: none,
43+
});
44+
//tslint:enable no-use-before-declare
45+
}
46+
const totalA = progressA.total.value;
47+
const totalB = progressB.total.value;
48+
const total = totalA + totalB;
49+
const loaded = (progressA.loaded * totalA + progressB.loaded * totalB) / (total * total);
50+
//tslint:disable no-use-before-declare
51+
return progress({
52+
loaded,
53+
total: some(total),
54+
});
55+
//tslint:enable no-use-before-declare
56+
}
57+
const noA = a.progress.isNone();
58+
const noB = b.progress.isNone();
59+
if (noA && !noB) {
60+
return b;
61+
}
62+
if (!noA && noB) {
63+
return a;
64+
}
65+
return pending; //tslint:disable-line no-use-before-declare
66+
};
67+
3068
export class RemoteInitial<L, A> {
3169
readonly _tag: 'RemoteInitial' = 'RemoteInitial';
3270
// prettier-ignore
@@ -364,7 +402,7 @@ export class RemoteFailure<L, A> {
364402
* `failure(new Error('err text')).ap(initial) will return initial.`
365403
*/
366404
ap<B>(fab: RemoteData<L, Function1<A, B>>): RemoteData<L, B> {
367-
return fab.fold(initial, pending, () => fab as any, () => this); //tslint:disable-line no-use-before-declare
405+
return fab.fold(initial, fab, () => fab as any, () => this); //tslint:disable-line no-use-before-declare
368406
}
369407

370408
/**
@@ -650,7 +688,7 @@ export class RemoteSuccess<L, A> {
650688
* `failure(new Error('err text')).ap(initial) will return initial.`
651689
*/
652690
ap<B>(fab: RemoteData<L, Function1<A, B>>): RemoteData<L, B> {
653-
return fab.fold(initial, pending, () => fab as any, value => this.map(value)); //tslint:disable-line no-use-before-declare
691+
return fab.fold(initial, fab, () => fab as any, value => this.map(value)); //tslint:disable-line no-use-before-declare
654692
}
655693

656694
/**
@@ -892,6 +930,8 @@ export class RemotePending<L, A> {
892930
// prettier-ignore
893931
readonly '_L': L;
894932

933+
constructor(readonly progress: Option<RemoteProgress> = none) {}
934+
895935
/**
896936
* `alt` short for alternative, takes another `RemoteData`.
897937
* If `this` `RemoteData` is a `RemoteSuccess` type then it will be returned.
@@ -934,7 +974,12 @@ export class RemotePending<L, A> {
934974
* `failure(new Error('err text')).ap(initial) will return initial.`
935975
*/
936976
ap<B>(fab: RemoteData<L, Function1<A, B>>): RemoteData<L, B> {
937-
return fab.fold(initial, pending as any, () => pending, () => pending); //tslint:disable-line no-use-before-declare
977+
return fab.fold(
978+
initial, //tslint:disable-line no-use-before-declare
979+
fab.isPending() ? (concatPendings(this, fab as any) as any) : this,
980+
() => this,
981+
() => this,
982+
);
938983
}
939984

940985
/**
@@ -1222,6 +1267,7 @@ const extend = <L, A, B>(fla: RemoteData<L, A>, f: Function1<RemoteData<L, A>, B
12221267
export const failure = <L, A>(error: L): RemoteFailure<L, A> => new RemoteFailure(error);
12231268
export const success: <L, A>(value: A) => RemoteSuccess<L, A> = of;
12241269
export const pending: RemotePending<never, never> = new RemotePending<never, never>();
1270+
export const progress = <L, A>(progress: RemoteProgress): RemotePending<L, A> => new RemotePending(some(progress));
12251271
export const initial: RemoteInitial<never, never> = new RemoteInitial<never, never>();
12261272

12271273
//Alternative
@@ -1272,7 +1318,7 @@ export const getSemigroup = <L, A>(SL: Semigroup<L>, SA: Semigroup<A>): Semigrou
12721318
concat: (x, y) => {
12731319
return x.foldL(
12741320
() => y.fold(y, y, () => y, () => y),
1275-
() => y.fold(x, y, () => y, () => y),
1321+
() => y.foldL(() => x, () => concatPendings(x as any, y as any), () => y, () => y),
12761322

12771323
xError => y.fold(x, x, yError => failure(SL.concat(xError, yError)), () => y),
12781324
xValue => y.fold(x, x, () => x, yValue => success(SA.concat(xValue, yValue))),
@@ -1312,6 +1358,13 @@ export function fromPredicate<L, A>(
13121358
return a => (predicate(a) ? success(a) : failure(whenFalse(a)));
13131359
}
13141360

1361+
export function fromProgressEvent<L, A>(event: ProgressEvent): RemotePending<L, A> {
1362+
return progress({
1363+
loaded: event.loaded,
1364+
total: event.lengthComputable ? some(event.total) : none,
1365+
});
1366+
}
1367+
13151368
//instance
13161369
export const remoteData: Monad2<URI> &
13171370
Foldable2<URI> &

0 commit comments

Comments
 (0)