From d1356ab41b62d75538df95d8682dbc3bcb411c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20H=C4=83loiu?= Date: Tue, 17 Mar 2020 15:51:59 +0100 Subject: [PATCH] Add resetOnSuccess option to retryBackoff operator --- spec/retryBackoff-spec.ts | 48 +++++++++++++++++++++++++++++++++++ src/operators/retryBackoff.ts | 32 +++++++++++++++-------- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/spec/retryBackoff-spec.ts b/spec/retryBackoff-spec.ts index db13abb..0435ae8 100644 --- a/spec/retryBackoff-spec.ts +++ b/spec/retryBackoff-spec.ts @@ -399,4 +399,52 @@ describe('retryBackoff operator', () => { expectSubscriptions(source.subscriptions).toBe(subs); }); }); + + it('should reset the delay when resetOnSuccess is true', () => { + testScheduler.run(({ expectObservable, cold, expectSubscriptions }) => { + const source = cold('--1-2-3-#'); + const subs = [ + ' ^-------!', + ' ---------^-------!', + ' ------------------^-------!', + ' ---------------------------^-------!' + // interval always reset to 1 ^ + ]; + const unsub = ' -----------------------------------!'; + const expected = ' --1-2-3----1-2-3----1-2-3----1-2-3--'; + + expectObservable( + source.pipe( + retryBackoff({ + initialInterval: 1, + resetOnSuccess: true + }) + ), + unsub + ).toBe(expected); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); + + it('should not reset the delay on consecutive errors when resetOnSuccess is true', () => { + testScheduler.run(({ expectObservable, cold, expectSubscriptions }) => { + const source = cold('--------#'); + const unsub = ' -------------------------------------!'; + const subs = [ + ' ^-------! ', + ' ---------^-------! ', + ' -------------------^-------! ', + ' -------------------------------^-----!' + ]; + const expected = ' --------------------------------------'; + + const result = source.pipe(retryBackoff({ + initialInterval: 1, + resetOnSuccess: true + })); + + expectObservable(result, unsub).toBe(expected); + expectSubscriptions(source.subscriptions).toBe(subs); + }); + }); }); diff --git a/src/operators/retryBackoff.ts b/src/operators/retryBackoff.ts index f7cd394..bfc0538 100644 --- a/src/operators/retryBackoff.ts +++ b/src/operators/retryBackoff.ts @@ -1,7 +1,7 @@ import { iif, Observable, throwError, timer } from 'rxjs'; -import { concatMap, retryWhen } from 'rxjs/operators'; +import { concatMap, retryWhen, tap } from 'rxjs/operators'; +import { exponentialBackoffDelay, getDelay } from '../utils'; -import { getDelay, exponentialBackoffDelay } from '../utils'; export interface RetryBackoffConfig { // Initial interval. It will eventually go as high as maxInterval. @@ -10,6 +10,9 @@ export interface RetryBackoffConfig { maxRetries?: number; // Maximum delay between retries. maxInterval?: number; + // When set to `true` every successful emission will reset the delay and the + // error count. + resetOnSuccess?: boolean; // Conditional retry. shouldRetry?: (error: any) => boolean; backoffDelay?: (iteration: number, initialInterval: number) => number; @@ -31,20 +34,29 @@ export function retryBackoff( maxRetries = Infinity, maxInterval = Infinity, shouldRetry = () => true, + resetOnSuccess = false, backoffDelay = exponentialBackoffDelay } = typeof config === 'number' ? { initialInterval: config } : config; - return (source: Observable) => - source.pipe( + return (source: Observable) => { + let index = 0; + return source.pipe( retryWhen(errors => errors.pipe( - concatMap((error, i) => - iif( - () => i < maxRetries && shouldRetry(error), - timer(getDelay(backoffDelay(i, initialInterval), maxInterval)), + concatMap(error => { + const attempt = index++; + return iif( + () => attempt < maxRetries && shouldRetry(error), + timer(getDelay(backoffDelay(attempt, initialInterval), maxInterval)), throwError(error) ) - ) + }) ) - ) + ), + tap((_: T) => { + if (resetOnSuccess) { + index = 0; + } + }) ); + } }