import { strict as assert } from "assert" ; import sinon from "sinon" ; import { formExponentialFn , formExponentialFnWithAttemptOffset , formLinearFn , Throttler , } from "../throttler" ; describe ( "Throttler" , ( ) => { let throttler ; let clock ; before ( ( ) => clock = sinon . useFakeTimers ( ) ) ; after ( ( ) => clock . restore ( ) ) ; afterEach ( ( ) => clock . reset ( ) ) ; function assertAscending ( array ) { if ( array . length < 1 ) { return ; } let prev = array [ 0 ] ; for ( const item of array ) { assert ( item >= prev , "out of order" ) ; prev = item ; } } function getDelayAndTick ( ) { const delay = throttler . getDelay ( ) ; clock . tick ( delay ) ; assert . strictEqual ( throttler . latestAttemptTime , Date . now ( ) , "getDelayAndTick should yield latestAttemptTime === now" ) ; assertAscending ( throttler . getAttempts ( ) ) ; return delay ; } function runTests ( { message , delayWindowMs , maxDelayMs , delayFn , expectedDelays , } ) { describe ( message , ( ) => { beforeEach ( ( ) => throttler = new Throttler ( delayWindowMs , maxDelayMs , delayFn ) ) ; const expectedMaxAttempts = expectedDelays . length ; const expectedDelayAt = ( attempt ) => attempt >= expectedMaxAttempts ? maxDelayMs : expectedDelays [ attempt ] ; it ( "Should initially have zero delay" , ( ) => { assert . strictEqual ( throttler . getDelay ( ) , 0 ) ; } ) ; it ( "Should increase as expected with instant failures" , ( ) => { for ( const expectedDelay of expectedDelays . concat ( [ maxDelayMs , maxDelayMs , maxDelayMs , maxDelayMs ] ) ) { assert . strictEqual ( getDelayAndTick ( ) , expectedDelay ) ; } } ) ; it ( "Should remain zero delay with long pauses between getDelay calls" , ( ) => { for ( let i = 0 ; i < 5 ; i ++ ) { assert . strictEqual ( getDelayAndTick ( ) , 0 , `iteration ${ i }` ) ; clock . tick ( delayWindowMs ) ; } assert . strictEqual ( getDelayAndTick ( ) , 0 ) ; clock . tick ( delayWindowMs - 1 ) ; assert . strictEqual ( getDelayAndTick ( ) , expectedDelayAt ( 1 ) ) ; } ) ; it ( "Should not increase with long pauses between getDelay calls" , ( ) => { const oneThirdTicks = Math . floor ( delayWindowMs / 3 ) ; const remainingTicks = delayWindowMs - ( 2 * oneThirdTicks ) ; assert . strictEqual ( getDelayAndTick ( ) , 0 ) ; clock . tick ( oneThirdTicks ) ; assert . strictEqual ( getDelayAndTick ( ) , expectedDelayAt ( 1 ) ) ; clock . tick ( oneThirdTicks ) ; assert . strictEqual ( getDelayAndTick ( ) , expectedDelayAt ( 2 ) ) ; clock . tick ( remainingTicks ) ; for ( let i = 0 ; i < 100 ; i ++ ) { assert . strictEqual ( getDelayAndTick ( ) , expectedDelays [ 2 ] , `iteration ${ i }` ) ; clock . tick ( i % 3 === 2 ? remainingTicks : oneThirdTicks ) ; } assert . strictEqual ( getDelayAndTick ( ) , expectedDelayAt ( 2 ) ) ; assert . strictEqual ( getDelayAndTick ( ) , expectedDelayAt ( 3 ) ) ; } ) ; it ( "Should stop increasing number of attempts after max" , ( ) => { for ( let i = 0 ; i < expectedMaxAttempts ; i ++ ) { getDelayAndTick ( ) ; assert . strictEqual ( throttler . numAttempts , i + 1 , `loop 1; iteration ${ i }` ) ; } for ( let i = 0 ; i < 100 ; i ++ ) { getDelayAndTick ( ) ; assert . strictEqual ( throttler . numAttempts , expectedMaxAttempts , `loop 2; iteration ${ i }` ) ; } } ) ; it ( "State should be corrected if delay is bypassed" , ( ) => { assert . strictEqual ( getDelayAndTick ( ) , 0 ) ; assert . strictEqual ( throttler . getDelay ( ) , expectedDelayAt ( 1 ) ) ; assert . strictEqual ( getDelayAndTick ( ) , expectedDelayAt ( 2 ) ) ; } ) ; } ) ; } runTests ( { message : "Exponential Delay" , delayWindowMs : 60 * 1000 , maxDelayMs : 30 * 1000 , /** f(n) = C x (B^n - G) + F = C x B^n + (F - C x G) = C x B^n - C */ delayFn : formExponentialFn ( { coefficient : 20 , offset : - 20 } ) , expectedDelays : [ 0 , 20 , 60 , 140 , 300 , 620 , 1260 , 2540 , 5100 , 10220 , 20460 , ] } ) ; runTests ( { message : "Exponential Delay with attempt offset" , delayWindowMs : 60 * 1000 , maxDelayMs : 30 * 1000 , delayFn : formExponentialFnWithAttemptOffset ( - 1 , { coefficient : 20 , initialDelay : 0 } ) , expectedDelays : [ 0 , 20 , 40 , 80 , 160 , 320 , 640 , 1280 , 2560 , 5120 , 10240 , 20480 , ] } ) ; runTests ( { message : "Linear Delay" , delayWindowMs : 60 , maxDelayMs : 30 , delayFn : formLinearFn ( { coefficient : 10 } ) , expectedDelays : [ 0 , 10 , 20 ] } ) ; } ) ;