Skip to content

Commit ab6e9fc

Browse files
authoredFeb 3, 2020
feat(retry): add config to reset error count on successful emission (#5280)
* feat(retry): add config to reset error count on successful emission This PR adds the ability to reset the error counter on successful emissions using the `retry` operator. The current behavior for `retry(n)` is to call error if n errors occurred, regardless of whether or not they were consecutive. Now one would be able to use `retry(n, true)` to have the count reset so that only n consecutive errors will cause the observable to fail. * feat(retry): add config parameter added overloaded signature to the `retry` operator that accepts a config object * chore: restore package-lock.json * chore: appease TypeScript * chore: revert change to Observable spec
1 parent 5170728 commit ab6e9fc

File tree

2 files changed

+115
-7
lines changed

2 files changed

+115
-7
lines changed
 

‎spec/operators/retry-spec.ts

+81-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect } from 'chai';
22
import { hot, cold, expectObservable, expectSubscriptions } from '../helpers/marble-testing';
33
import { retry, map, take, mergeMap, concat, multicast, refCount } from 'rxjs/operators';
4-
import { Observable, Observer, of, throwError, Subject } from 'rxjs';
4+
import { Observable, Observer, defer, range, of, throwError, Subject } from 'rxjs';
55

66
declare function asDiagram(arg: string): Function;
77

@@ -58,16 +58,94 @@ describe('retry operator', () => {
5858
retry(retries - 1)
5959
).subscribe(
6060
(x: number) => {
61-
expect(x).to.equal(42);
61+
done("shouldn't next");
6262
},
6363
(err: any) => {
6464
expect(errors).to.equal(2);
6565
done();
6666
}, () => {
67-
expect('this was called').to.be.true;
67+
done("shouldn't complete");
6868
});
6969
});
7070

71+
it('should retry a number of times, then call error handler (with resetOnSuccess)', (done: MochaDone) => {
72+
let errors = 0;
73+
const retries = 2;
74+
Observable.create((observer: Observer<number>) => {
75+
observer.next(42);
76+
observer.complete();
77+
}).pipe(
78+
map((x: any) => {
79+
errors += 1;
80+
throw 'bad';
81+
}),
82+
retry({count: retries - 1, resetOnSuccess: true})
83+
).subscribe(
84+
(x: number) => {
85+
done("shouldn't next");
86+
},
87+
(err: any) => {
88+
expect(errors).to.equal(2);
89+
done();
90+
}, () => {
91+
done("shouldn't complete");
92+
});
93+
});
94+
95+
it('should retry a number of times, then call next handler without error, then retry and complete', (done: MochaDone) => {
96+
let index = 0;
97+
let errors = 0;
98+
const retries = 2;
99+
defer(() => range(0, 4 - index)).pipe(
100+
mergeMap(() => {
101+
index++;
102+
if (index === 1 || index === 3) {
103+
errors++;
104+
return throwError('bad');
105+
} else {
106+
return of(42);
107+
}
108+
}),
109+
retry({count: retries - 1, resetOnSuccess: true})
110+
).subscribe(
111+
(x: number) => {
112+
expect(x).to.equal(42);
113+
},
114+
(err: any) => {
115+
done("shouldn't error");
116+
}, () => {
117+
expect(errors).to.equal(retries);
118+
done();
119+
});
120+
});
121+
122+
it('should retry a number of times, then call next handler without error, then retry and error', (done: MochaDone) => {
123+
let index = 0;
124+
let errors = 0;
125+
const retries = 2;
126+
defer(() => range(0, 4 - index)).pipe(
127+
mergeMap(() => {
128+
index++;
129+
if (index === 1 || index === 3) {
130+
errors++;
131+
return throwError('bad');
132+
} else {
133+
return of(42);
134+
}
135+
}),
136+
retry({count: retries - 1, resetOnSuccess: false})
137+
).subscribe(
138+
(x: number) => {
139+
expect(x).to.equal(42);
140+
},
141+
(err: any) => {
142+
expect(errors).to.equal(retries);
143+
done();
144+
}, () => {
145+
done("shouldn't complete");
146+
});
147+
});
148+
71149
it('should retry until successful completion', (done: MochaDone) => {
72150
let errors = 0;
73151
const retries = 10;

‎src/internal/operators/retry.ts

+34-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { Observable } from '../Observable';
44

55
import { MonoTypeOperatorFunction, TeardownLogic } from '../types';
66

7+
export interface RetryConfig {
8+
count: number;
9+
resetOnSuccess?: boolean;
10+
}
11+
712
/**
813
* Returns an Observable that mirrors the source Observable with the exception of an `error`. If the source Observable
914
* calls `error`, this method will resubscribe to the source Observable for a maximum of `count` resubscriptions (given
@@ -46,21 +51,33 @@ import { MonoTypeOperatorFunction, TeardownLogic } from '../types';
4651
* ```
4752
*
4853
* @param {number} count - Number of retry attempts before failing.
54+
* @param {boolean} resetOnSuccess - When set to `true` every successful emission will reset the error count
4955
* @return {Observable} The source Observable modified with the retry logic.
5056
* @method retry
5157
* @owner Observable
5258
*/
53-
export function retry<T>(count: number = -1): MonoTypeOperatorFunction<T> {
54-
return (source: Observable<T>) => source.lift(new RetryOperator(count, source));
59+
export function retry<T>(count?: number): MonoTypeOperatorFunction<T>;
60+
export function retry<T>(config: RetryConfig): MonoTypeOperatorFunction<T>;
61+
export function retry<T>(configOrCount: number | RetryConfig = -1): MonoTypeOperatorFunction<T> {
62+
let config: RetryConfig;
63+
if (configOrCount && typeof configOrCount === 'object') {
64+
config = configOrCount as RetryConfig;
65+
} else {
66+
config = {
67+
count: configOrCount as number
68+
};
69+
}
70+
return (source: Observable<T>) => source.lift(new RetryOperator(config.count, !!config.resetOnSuccess, source));
5571
}
5672

5773
class RetryOperator<T> implements Operator<T, T> {
5874
constructor(private count: number,
75+
private resetOnSuccess: boolean,
5976
private source: Observable<T>) {
6077
}
6178

6279
call(subscriber: Subscriber<T>, source: any): TeardownLogic {
63-
return source.subscribe(new RetrySubscriber(subscriber, this.count, this.source));
80+
return source.subscribe(new RetrySubscriber(subscriber, this.count, this.resetOnSuccess, this.source));
6481
}
6582
}
6683

@@ -70,11 +87,24 @@ class RetryOperator<T> implements Operator<T, T> {
7087
* @extends {Ignored}
7188
*/
7289
class RetrySubscriber<T> extends Subscriber<T> {
90+
private readonly initialCount: number;
91+
7392
constructor(destination: Subscriber<any>,
7493
private count: number,
75-
private source: Observable<T>) {
94+
private resetOnSuccess: boolean,
95+
private source: Observable<T>
96+
) {
7697
super(destination);
98+
this.initialCount = this.count;
7799
}
100+
101+
next(value?: T): void {
102+
super.next(value);
103+
if (this.resetOnSuccess) {
104+
this.count = this.initialCount;
105+
}
106+
}
107+
78108
error(err: any) {
79109
if (!this.isStopped) {
80110
const { source, count } = this;

0 commit comments

Comments
 (0)
Please sign in to comment.