Comparing version 1.1.1 to 2.0.0
# Changelog | ||
## 2.0.0 - 2020-09-24 | ||
- **breaking:** **reactor:** introduce a separate BackoffFactory interface for the first backoff | ||
This _only_ requires changes if you use retry policies in your own code, outside of the `Policy.retry()`. | ||
See [#30](https://github.com/connor4312/cockatiel/issues/30). For some backoff policies, such as delegate and exponential policies, the first backoff was always 0, before `next()` was called. This is undesirable, and fixing it involved separating the backoff factory from the backoff itself. | ||
The backoff classes, such as `DelegateBackoff` and `ExponentialBackoff`, now _only_ have a `next()` method. The `duration`, which is now a property instead of a method, is only available after the first `next()` call. | ||
For example, previously if you did this: | ||
```js | ||
let backoff = new ExponentialBackoff(); | ||
while (!succeeded) { | ||
if (!tryAgain()) { | ||
await delay(backoff.duration()); | ||
backoff = backoff.next(); | ||
} | ||
} | ||
``` | ||
You now need to call `next()` before you access `duration`: | ||
```js | ||
let backoff = new ExponentialBackoff(); | ||
while (!succeeded) { | ||
if (!tryAgain()) { | ||
backoff = backoff.next(); | ||
await delay(backoff.duration); | ||
} | ||
} | ||
``` | ||
> Note: if you use typescript, you will need another variable for it to understand you. [Here's an example](https://github.com/connor4312/cockatiel/blob/657be03da7ff6d5fa68da4a0a4172e217882b6bc/src/RetryPolicy.ts#L149-L163) of how we use it inside the RetryPolicy. | ||
## 1.1.1 - 2020-07-17 | ||
@@ -4,0 +40,0 @@ |
/** | ||
* A generic type that returns backoff intervals. | ||
*/ | ||
export interface IBackoff<T> { | ||
export interface IBackoffFactory<T> { | ||
/** | ||
* Returns the number of milliseconds to wait for this backoff attempt. | ||
* Returns the first backoff duration. Can return "undefined" to signal | ||
* that we should not back off. | ||
*/ | ||
duration(): number; | ||
next(context: T): IBackoff<T> | undefined; | ||
} | ||
/** | ||
* A generic type that returns backoff intervals. | ||
*/ | ||
export interface IBackoff<T> extends IBackoffFactory<T> { | ||
/** | ||
* Returns the next backoff duration. Can return "undefined" to signal | ||
* that we should stop backing off. | ||
* Returns the number of milliseconds to wait for this backoff attempt. | ||
*/ | ||
next(context: T): IBackoff<T> | undefined; | ||
readonly duration: number; | ||
} | ||
@@ -15,0 +20,0 @@ export * from './CompositeBackoff'; |
@@ -1,2 +0,2 @@ | ||
import { IBackoff } from './Backoff'; | ||
export declare const expectDurations: <T>(backoff: IBackoff<T> | undefined, expected: readonly (number | undefined)[], context?: T | undefined) => void; | ||
import { IBackoffFactory } from './Backoff'; | ||
export declare const expectDurations: <T>(backoffFactory: IBackoffFactory<T> | undefined, expected: readonly (number | undefined)[], context?: T | undefined) => void; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const chai_1 = require("chai"); | ||
exports.expectDurations = (backoff, expected, context) => { | ||
exports.expectDurations = (backoffFactory, expected, context) => { | ||
var _a, _b; | ||
const actual = []; | ||
let backoff = (_a = backoffFactory) === null || _a === void 0 ? void 0 : _a.next(context); | ||
// tslint:disable-next-line: prefer-for-of | ||
@@ -12,3 +14,3 @@ for (let i = 0; i < expected.length; i++) { | ||
} | ||
actual.push(backoff.duration()); | ||
actual.push((_b = backoff) === null || _b === void 0 ? void 0 : _b.duration); | ||
backoff = backoff.next(context); | ||
@@ -15,0 +17,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { IBackoff } from './Backoff'; | ||
import { IBackoff, IBackoffFactory } from './Backoff'; | ||
export declare type CompositeBias = 'a' | 'b' | 'max' | 'min'; | ||
@@ -8,15 +8,11 @@ /** | ||
*/ | ||
export declare class CompositeBackoff<T> implements IBackoff<T> { | ||
export declare class CompositeBackoff<T> implements IBackoffFactory<T> { | ||
private readonly bias; | ||
private readonly backoffA; | ||
private readonly backoffB; | ||
constructor(bias: CompositeBias, backoffA: IBackoff<T>, backoffB: IBackoff<T>); | ||
constructor(bias: CompositeBias, backoffA: IBackoffFactory<T>, backoffB: IBackoffFactory<T>); | ||
/** | ||
* @inheritdoc | ||
*/ | ||
duration(): number; | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next(context: T): CompositeBackoff<T> | undefined; | ||
next(context: T): IBackoff<T> | undefined; | ||
} |
@@ -17,16 +17,27 @@ "use strict"; | ||
*/ | ||
duration() { | ||
switch (this.bias) { | ||
next(context) { | ||
const nextA = this.backoffA.next(context); | ||
const nextB = this.backoffB.next(context); | ||
return nextA && nextB && instance(this.bias, nextA, nextB); | ||
} | ||
} | ||
exports.CompositeBackoff = CompositeBackoff; | ||
const instance = (bias, backoffA, backoffB) => ({ | ||
/** | ||
* @inheritdoc | ||
*/ | ||
get duration() { | ||
switch (bias) { | ||
case 'a': | ||
return this.backoffA.duration(); | ||
return backoffA.duration; | ||
case 'b': | ||
return this.backoffB.duration(); | ||
return backoffB.duration; | ||
case 'max': | ||
return Math.max(this.backoffB.duration(), this.backoffA.duration()); | ||
return Math.max(backoffB.duration, backoffA.duration); | ||
case 'min': | ||
return Math.min(this.backoffB.duration(), this.backoffA.duration()); | ||
return Math.min(backoffB.duration, backoffA.duration); | ||
default: | ||
throw new Error(`Unknown bias "${this.bias}" given to CompositeBackoff`); | ||
throw new Error(`Unknown bias "${bias}" given to CompositeBackoff`); | ||
} | ||
} | ||
}, | ||
/** | ||
@@ -36,8 +47,7 @@ * @inheritdoc | ||
next(context) { | ||
const nextA = this.backoffA.next(context); | ||
const nextB = this.backoffB.next(context); | ||
return nextA && nextB && new CompositeBackoff(this.bias, nextA, nextB); | ||
} | ||
} | ||
exports.CompositeBackoff = CompositeBackoff; | ||
const nextA = backoffA.next(context); | ||
const nextB = backoffB.next(context); | ||
return nextA && nextB && instance(bias, nextA, nextB); | ||
}, | ||
}); | ||
//# sourceMappingURL=CompositeBackoff.js.map |
@@ -10,6 +10,7 @@ "use strict"; | ||
it('biases correctly', () => { | ||
chai_1.expect(withBias('a').duration()).to.equal(10); | ||
chai_1.expect(withBias('b').duration()).to.equal(20); | ||
chai_1.expect(withBias('min').duration()).to.equal(10); | ||
chai_1.expect(withBias('max').duration()).to.equal(20); | ||
var _a, _b, _c, _d; | ||
chai_1.expect((_a = withBias('a').next(undefined)) === null || _a === void 0 ? void 0 : _a.duration).to.equal(10); | ||
chai_1.expect((_b = withBias('b').next(undefined)) === null || _b === void 0 ? void 0 : _b.duration).to.equal(20); | ||
chai_1.expect((_c = withBias('min').next(undefined)) === null || _c === void 0 ? void 0 : _c.duration).to.equal(10); | ||
chai_1.expect((_d = withBias('max').next(undefined)) === null || _d === void 0 ? void 0 : _d.duration).to.equal(20); | ||
}); | ||
@@ -16,0 +17,0 @@ it('limits the number of retries', () => { |
@@ -1,9 +0,8 @@ | ||
import { IBackoff } from './Backoff'; | ||
import { IBackoff, IBackoffFactory } from './Backoff'; | ||
/** | ||
* Backoff that returns a constant interval. | ||
*/ | ||
export declare class ConstantBackoff implements IBackoff<void> { | ||
export declare class ConstantBackoff implements IBackoffFactory<unknown> { | ||
private readonly interval; | ||
private readonly limit?; | ||
private index; | ||
constructor(interval: number, limit?: number | undefined); | ||
@@ -13,7 +12,3 @@ /** | ||
*/ | ||
duration(): number; | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next(): ConstantBackoff | undefined; | ||
next(): IBackoff<unknown>; | ||
} | ||
@@ -20,0 +15,0 @@ /** |
@@ -10,3 +10,2 @@ "use strict"; | ||
this.limit = limit; | ||
this.index = 0; | ||
} | ||
@@ -16,18 +15,4 @@ /** | ||
*/ | ||
duration() { | ||
return this.interval; | ||
} | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next() { | ||
if (this.limit === undefined) { | ||
return this; | ||
} | ||
if (this.index >= this.limit - 1) { | ||
return undefined; | ||
} | ||
const b = new ConstantBackoff(this.interval, this.limit); | ||
b.index = this.index + 1; | ||
return b; | ||
return instance(this.interval, this.limit); | ||
} | ||
@@ -40,2 +25,14 @@ } | ||
exports.NeverRetryBackoff = new ConstantBackoff(0, 0); | ||
const instance = (interval, limit, index = 0) => ({ | ||
duration: interval, | ||
next() { | ||
if (limit === undefined) { | ||
return this; | ||
} | ||
if (index >= limit - 1) { | ||
return undefined; | ||
} | ||
return instance(interval, limit, index + 1); | ||
}, | ||
}); | ||
//# sourceMappingURL=ConstantBackoff.js.map |
@@ -1,2 +0,2 @@ | ||
import { IBackoff } from './Backoff'; | ||
import { IBackoff, IBackoffFactory } from './Backoff'; | ||
export declare type DelegateBackoffFn<T, S = void> = (context: T, state?: S) => { | ||
@@ -11,15 +11,9 @@ delay: number; | ||
*/ | ||
export declare class DelegateBackoff<T, S = void> implements IBackoff<T> { | ||
export declare class DelegateBackoff<T, S = void> implements IBackoffFactory<T> { | ||
private readonly fn; | ||
private readonly state?; | ||
private current; | ||
constructor(fn: DelegateBackoffFn<T, S>, state?: S | undefined); | ||
constructor(fn: DelegateBackoffFn<T, S>); | ||
/** | ||
* @inheritdoc | ||
*/ | ||
duration(): number; | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next(context: T): DelegateBackoff<T, S> | undefined; | ||
next(context: T): IBackoff<T> | undefined; | ||
} |
@@ -9,6 +9,4 @@ "use strict"; | ||
class DelegateBackoff { | ||
constructor(fn, state) { | ||
constructor(fn) { | ||
this.fn = fn; | ||
this.state = state; | ||
this.current = 0; | ||
} | ||
@@ -18,26 +16,19 @@ /** | ||
*/ | ||
duration() { | ||
return this.current; | ||
next(context) { | ||
return instance(this.fn).next(context); | ||
} | ||
/** | ||
* @inheritdoc | ||
*/ | ||
} | ||
exports.DelegateBackoff = DelegateBackoff; | ||
const instance = (fn, state, current = 0) => ({ | ||
duration: current, | ||
next(context) { | ||
const result = this.fn(context, this.state); | ||
const result = fn(context, state); | ||
if (result === undefined) { | ||
return undefined; | ||
} | ||
let b; | ||
if (typeof result === 'number') { | ||
b = new DelegateBackoff(this.fn, this.state); | ||
b.current = result; | ||
} | ||
else { | ||
b = new DelegateBackoff(this.fn, result.state); | ||
b.current = result.delay; | ||
} | ||
return b; | ||
} | ||
} | ||
exports.DelegateBackoff = DelegateBackoff; | ||
return typeof result === 'number' | ||
? instance(fn, state, result) | ||
: instance(fn, result.state, result.delay); | ||
}, | ||
}); | ||
//# sourceMappingURL=DelegateBackoff.js.map |
@@ -9,3 +9,3 @@ "use strict"; | ||
const b = new DelegateBackoff_1.DelegateBackoff(v => v * 2); | ||
chai_1.expect(b.next(4).duration()).to.equal(8); | ||
chai_1.expect(b.next(4).duration).to.equal(8); | ||
}); | ||
@@ -21,5 +21,5 @@ it('halts delegate function returns undefined', () => { | ||
}); | ||
Backoff_test_1.expectDurations(b, [0, 9, 81, 6561]); | ||
Backoff_test_1.expectDurations(b, [9, 81, 6561, 43046721]); | ||
}); | ||
}); | ||
//# sourceMappingURL=DelegateBackoff.test.js.map |
@@ -1,2 +0,2 @@ | ||
import { IBackoff } from './Backoff'; | ||
import { IBackoff, IBackoffFactory } from './Backoff'; | ||
import { GeneratorFn } from './ExponentialBackoffGenerators'; | ||
@@ -37,13 +37,6 @@ /** | ||
*/ | ||
export declare class ExponentialBackoff<S> implements IBackoff<void> { | ||
private options; | ||
private state?; | ||
private attempt; | ||
private delay; | ||
export declare class ExponentialBackoff<S> implements IBackoffFactory<unknown> { | ||
private readonly options; | ||
constructor(options?: Partial<IExponentialBackoffOptions<S>>); | ||
/** | ||
* @inheritdoc | ||
*/ | ||
duration(): number; | ||
next(): ExponentialBackoff<S> | undefined; | ||
next(): IBackoff<unknown> | undefined; | ||
} |
@@ -16,23 +16,22 @@ "use strict"; | ||
constructor(options) { | ||
this.attempt = 0; | ||
this.delay = 0; | ||
this.options = options ? { ...defaultOptions, ...options } : defaultOptions; | ||
} | ||
/** | ||
* @inheritdoc | ||
*/ | ||
duration() { | ||
return this.delay; | ||
next() { | ||
return instance(this.options).next(undefined); | ||
} | ||
} | ||
exports.ExponentialBackoff = ExponentialBackoff; | ||
/** | ||
* An implementation of exponential backoff. | ||
*/ | ||
const instance = (options, state, delay = 0, attempt = -1) => ({ | ||
duration: delay, | ||
next() { | ||
if (this.attempt >= this.options.maxAttempts - 1) { | ||
if (attempt >= options.maxAttempts - 1) { | ||
return undefined; | ||
} | ||
const e = new ExponentialBackoff(this.options); | ||
[e.delay, e.state] = this.options.generator(this.state, this.options); | ||
e.attempt = this.attempt + 1; | ||
return e; | ||
} | ||
} | ||
exports.ExponentialBackoff = ExponentialBackoff; | ||
const [nextDelay, nextState] = options.generator(state, options); | ||
return instance(options, nextState, nextDelay, attempt + 1); | ||
}, | ||
}); | ||
//# sourceMappingURL=ExponentialBackoff.js.map |
@@ -9,9 +9,9 @@ "use strict"; | ||
const b = new ExponentialBackoff_1.ExponentialBackoff({ generator: ExponentialBackoffGenerators_1.noJitterGenerator }); | ||
Backoff_test_1.expectDurations(b, [0, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 30000, 30000]); | ||
Backoff_test_1.expectDurations(b, [128, 256, 512, 1024, 2048, 4096, 8192, 16384, 30000, 30000, 30000]); | ||
}); | ||
it('sets max retries correctly', () => { | ||
const b = new ExponentialBackoff_1.ExponentialBackoff({ generator: ExponentialBackoffGenerators_1.noJitterGenerator, maxAttempts: 4 }); | ||
Backoff_test_1.expectDurations(b, [0, 128, 256, 512, undefined]); | ||
Backoff_test_1.expectDurations(b, [128, 256, 512, 1024, undefined]); | ||
}); | ||
}); | ||
//# sourceMappingURL=ExponentialBackoff.test.js.map |
@@ -1,17 +0,12 @@ | ||
import { IBackoff } from './Backoff'; | ||
import { IBackoff, IBackoffFactory } from './Backoff'; | ||
/** | ||
* Backoff that returns a number from an iterable. | ||
*/ | ||
export declare class IterableBackoff implements IBackoff<void> { | ||
export declare class IterableBackoff implements IBackoffFactory<unknown> { | ||
private readonly durations; | ||
private readonly index; | ||
constructor(durations: ReadonlyArray<number>, index?: number); | ||
constructor(durations: ReadonlyArray<number>); | ||
/** | ||
* @inheritdoc | ||
*/ | ||
duration(): number; | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next(): IterableBackoff | undefined; | ||
next(): IBackoff<unknown>; | ||
} |
@@ -7,8 +7,4 @@ "use strict"; | ||
class IterableBackoff { | ||
constructor(durations, index = 0) { | ||
constructor(durations) { | ||
this.durations = durations; | ||
this.index = index; | ||
if (index >= durations.length) { | ||
throw new RangeError(`IterableBackoff index ${0} >= number of durations (${durations.length})`); | ||
} | ||
} | ||
@@ -18,15 +14,11 @@ /** | ||
*/ | ||
duration() { | ||
return this.durations[this.index]; | ||
} | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next() { | ||
return this.index < this.durations.length - 1 | ||
? new IterableBackoff(this.durations, this.index + 1) | ||
: undefined; | ||
return instance(this.durations, 0); | ||
} | ||
} | ||
exports.IterableBackoff = IterableBackoff; | ||
const instance = (durations, index) => ({ | ||
duration: durations[index], | ||
next: () => (index < durations.length - 1 ? instance(durations, index + 1) : undefined), | ||
}); | ||
//# sourceMappingURL=IterableBackoff.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const chai_1 = require("chai"); | ||
const Backoff_test_1 = require("./Backoff.test"); | ||
@@ -11,6 +10,3 @@ const IterableBackoff_1 = require("./IterableBackoff"); | ||
}); | ||
it('throws a range error if empty', () => { | ||
chai_1.expect(() => new IterableBackoff_1.IterableBackoff([])).to.throw(RangeError); | ||
}); | ||
}); | ||
//# sourceMappingURL=IterableBackoff.test.js.map |
/** | ||
* A generic type that returns backoff intervals. | ||
*/ | ||
export interface IBackoff<T> { | ||
export interface IBackoffFactory<T> { | ||
/** | ||
* Returns the number of milliseconds to wait for this backoff attempt. | ||
* Returns the first backoff duration. Can return "undefined" to signal | ||
* that we should not back off. | ||
*/ | ||
duration(): number; | ||
next(context: T): IBackoff<T> | undefined; | ||
} | ||
/** | ||
* A generic type that returns backoff intervals. | ||
*/ | ||
export interface IBackoff<T> extends IBackoffFactory<T> { | ||
/** | ||
* Returns the next backoff duration. Can return "undefined" to signal | ||
* that we should stop backing off. | ||
* Returns the number of milliseconds to wait for this backoff attempt. | ||
*/ | ||
next(context: T): IBackoff<T> | undefined; | ||
readonly duration: number; | ||
} | ||
@@ -15,0 +20,0 @@ export * from './CompositeBackoff'; |
@@ -1,2 +0,2 @@ | ||
import { IBackoff } from './Backoff'; | ||
export declare const expectDurations: <T>(backoff: IBackoff<T> | undefined, expected: readonly (number | undefined)[], context?: T | undefined) => void; | ||
import { IBackoffFactory } from './Backoff'; | ||
export declare const expectDurations: <T>(backoffFactory: IBackoffFactory<T> | undefined, expected: readonly (number | undefined)[], context?: T | undefined) => void; |
import { expect } from 'chai'; | ||
export const expectDurations = (backoff, expected, context) => { | ||
export const expectDurations = (backoffFactory, expected, context) => { | ||
var _a, _b; | ||
const actual = []; | ||
let backoff = (_a = backoffFactory) === null || _a === void 0 ? void 0 : _a.next(context); | ||
// tslint:disable-next-line: prefer-for-of | ||
@@ -10,3 +12,3 @@ for (let i = 0; i < expected.length; i++) { | ||
} | ||
actual.push(backoff.duration()); | ||
actual.push((_b = backoff) === null || _b === void 0 ? void 0 : _b.duration); | ||
backoff = backoff.next(context); | ||
@@ -13,0 +15,0 @@ } |
@@ -1,2 +0,2 @@ | ||
import { IBackoff } from './Backoff'; | ||
import { IBackoff, IBackoffFactory } from './Backoff'; | ||
export declare type CompositeBias = 'a' | 'b' | 'max' | 'min'; | ||
@@ -8,15 +8,11 @@ /** | ||
*/ | ||
export declare class CompositeBackoff<T> implements IBackoff<T> { | ||
export declare class CompositeBackoff<T> implements IBackoffFactory<T> { | ||
private readonly bias; | ||
private readonly backoffA; | ||
private readonly backoffB; | ||
constructor(bias: CompositeBias, backoffA: IBackoff<T>, backoffB: IBackoff<T>); | ||
constructor(bias: CompositeBias, backoffA: IBackoffFactory<T>, backoffB: IBackoffFactory<T>); | ||
/** | ||
* @inheritdoc | ||
*/ | ||
duration(): number; | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next(context: T): CompositeBackoff<T> | undefined; | ||
next(context: T): IBackoff<T> | undefined; | ||
} |
@@ -15,16 +15,26 @@ /** | ||
*/ | ||
duration() { | ||
switch (this.bias) { | ||
next(context) { | ||
const nextA = this.backoffA.next(context); | ||
const nextB = this.backoffB.next(context); | ||
return nextA && nextB && instance(this.bias, nextA, nextB); | ||
} | ||
} | ||
const instance = (bias, backoffA, backoffB) => ({ | ||
/** | ||
* @inheritdoc | ||
*/ | ||
get duration() { | ||
switch (bias) { | ||
case 'a': | ||
return this.backoffA.duration(); | ||
return backoffA.duration; | ||
case 'b': | ||
return this.backoffB.duration(); | ||
return backoffB.duration; | ||
case 'max': | ||
return Math.max(this.backoffB.duration(), this.backoffA.duration()); | ||
return Math.max(backoffB.duration, backoffA.duration); | ||
case 'min': | ||
return Math.min(this.backoffB.duration(), this.backoffA.duration()); | ||
return Math.min(backoffB.duration, backoffA.duration); | ||
default: | ||
throw new Error(`Unknown bias "${this.bias}" given to CompositeBackoff`); | ||
throw new Error(`Unknown bias "${bias}" given to CompositeBackoff`); | ||
} | ||
} | ||
}, | ||
/** | ||
@@ -34,7 +44,7 @@ * @inheritdoc | ||
next(context) { | ||
const nextA = this.backoffA.next(context); | ||
const nextB = this.backoffB.next(context); | ||
return nextA && nextB && new CompositeBackoff(this.bias, nextA, nextB); | ||
} | ||
} | ||
const nextA = backoffA.next(context); | ||
const nextB = backoffB.next(context); | ||
return nextA && nextB && instance(bias, nextA, nextB); | ||
}, | ||
}); | ||
//# sourceMappingURL=CompositeBackoff.js.map |
@@ -8,6 +8,7 @@ import { expect } from 'chai'; | ||
it('biases correctly', () => { | ||
expect(withBias('a').duration()).to.equal(10); | ||
expect(withBias('b').duration()).to.equal(20); | ||
expect(withBias('min').duration()).to.equal(10); | ||
expect(withBias('max').duration()).to.equal(20); | ||
var _a, _b, _c, _d; | ||
expect((_a = withBias('a').next(undefined)) === null || _a === void 0 ? void 0 : _a.duration).to.equal(10); | ||
expect((_b = withBias('b').next(undefined)) === null || _b === void 0 ? void 0 : _b.duration).to.equal(20); | ||
expect((_c = withBias('min').next(undefined)) === null || _c === void 0 ? void 0 : _c.duration).to.equal(10); | ||
expect((_d = withBias('max').next(undefined)) === null || _d === void 0 ? void 0 : _d.duration).to.equal(20); | ||
}); | ||
@@ -14,0 +15,0 @@ it('limits the number of retries', () => { |
@@ -1,9 +0,8 @@ | ||
import { IBackoff } from './Backoff'; | ||
import { IBackoff, IBackoffFactory } from './Backoff'; | ||
/** | ||
* Backoff that returns a constant interval. | ||
*/ | ||
export declare class ConstantBackoff implements IBackoff<void> { | ||
export declare class ConstantBackoff implements IBackoffFactory<unknown> { | ||
private readonly interval; | ||
private readonly limit?; | ||
private index; | ||
constructor(interval: number, limit?: number | undefined); | ||
@@ -13,7 +12,3 @@ /** | ||
*/ | ||
duration(): number; | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next(): ConstantBackoff | undefined; | ||
next(): IBackoff<unknown>; | ||
} | ||
@@ -20,0 +15,0 @@ /** |
@@ -8,3 +8,2 @@ /** | ||
this.limit = limit; | ||
this.index = 0; | ||
} | ||
@@ -14,18 +13,4 @@ /** | ||
*/ | ||
duration() { | ||
return this.interval; | ||
} | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next() { | ||
if (this.limit === undefined) { | ||
return this; | ||
} | ||
if (this.index >= this.limit - 1) { | ||
return undefined; | ||
} | ||
const b = new ConstantBackoff(this.interval, this.limit); | ||
b.index = this.index + 1; | ||
return b; | ||
return instance(this.interval, this.limit); | ||
} | ||
@@ -37,2 +22,14 @@ } | ||
export const NeverRetryBackoff = new ConstantBackoff(0, 0); | ||
const instance = (interval, limit, index = 0) => ({ | ||
duration: interval, | ||
next() { | ||
if (limit === undefined) { | ||
return this; | ||
} | ||
if (index >= limit - 1) { | ||
return undefined; | ||
} | ||
return instance(interval, limit, index + 1); | ||
}, | ||
}); | ||
//# sourceMappingURL=ConstantBackoff.js.map |
@@ -1,2 +0,2 @@ | ||
import { IBackoff } from './Backoff'; | ||
import { IBackoff, IBackoffFactory } from './Backoff'; | ||
export declare type DelegateBackoffFn<T, S = void> = (context: T, state?: S) => { | ||
@@ -11,15 +11,9 @@ delay: number; | ||
*/ | ||
export declare class DelegateBackoff<T, S = void> implements IBackoff<T> { | ||
export declare class DelegateBackoff<T, S = void> implements IBackoffFactory<T> { | ||
private readonly fn; | ||
private readonly state?; | ||
private current; | ||
constructor(fn: DelegateBackoffFn<T, S>, state?: S | undefined); | ||
constructor(fn: DelegateBackoffFn<T, S>); | ||
/** | ||
* @inheritdoc | ||
*/ | ||
duration(): number; | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next(context: T): DelegateBackoff<T, S> | undefined; | ||
next(context: T): IBackoff<T> | undefined; | ||
} |
@@ -7,6 +7,4 @@ /** | ||
export class DelegateBackoff { | ||
constructor(fn, state) { | ||
constructor(fn) { | ||
this.fn = fn; | ||
this.state = state; | ||
this.current = 0; | ||
} | ||
@@ -16,25 +14,18 @@ /** | ||
*/ | ||
duration() { | ||
return this.current; | ||
next(context) { | ||
return instance(this.fn).next(context); | ||
} | ||
/** | ||
* @inheritdoc | ||
*/ | ||
} | ||
const instance = (fn, state, current = 0) => ({ | ||
duration: current, | ||
next(context) { | ||
const result = this.fn(context, this.state); | ||
const result = fn(context, state); | ||
if (result === undefined) { | ||
return undefined; | ||
} | ||
let b; | ||
if (typeof result === 'number') { | ||
b = new DelegateBackoff(this.fn, this.state); | ||
b.current = result; | ||
} | ||
else { | ||
b = new DelegateBackoff(this.fn, result.state); | ||
b.current = result.delay; | ||
} | ||
return b; | ||
} | ||
} | ||
return typeof result === 'number' | ||
? instance(fn, state, result) | ||
: instance(fn, result.state, result.delay); | ||
}, | ||
}); | ||
//# sourceMappingURL=DelegateBackoff.js.map |
@@ -7,3 +7,3 @@ import { expect } from 'chai'; | ||
const b = new DelegateBackoff(v => v * 2); | ||
expect(b.next(4).duration()).to.equal(8); | ||
expect(b.next(4).duration).to.equal(8); | ||
}); | ||
@@ -19,5 +19,5 @@ it('halts delegate function returns undefined', () => { | ||
}); | ||
expectDurations(b, [0, 9, 81, 6561]); | ||
expectDurations(b, [9, 81, 6561, 43046721]); | ||
}); | ||
}); | ||
//# sourceMappingURL=DelegateBackoff.test.js.map |
@@ -1,2 +0,2 @@ | ||
import { IBackoff } from './Backoff'; | ||
import { IBackoff, IBackoffFactory } from './Backoff'; | ||
import { GeneratorFn } from './ExponentialBackoffGenerators'; | ||
@@ -37,13 +37,6 @@ /** | ||
*/ | ||
export declare class ExponentialBackoff<S> implements IBackoff<void> { | ||
private options; | ||
private state?; | ||
private attempt; | ||
private delay; | ||
export declare class ExponentialBackoff<S> implements IBackoffFactory<unknown> { | ||
private readonly options; | ||
constructor(options?: Partial<IExponentialBackoffOptions<S>>); | ||
/** | ||
* @inheritdoc | ||
*/ | ||
duration(): number; | ||
next(): ExponentialBackoff<S> | undefined; | ||
next(): IBackoff<unknown> | undefined; | ||
} |
@@ -14,22 +14,21 @@ import { decorrelatedJitterGenerator } from './ExponentialBackoffGenerators'; | ||
constructor(options) { | ||
this.attempt = 0; | ||
this.delay = 0; | ||
this.options = options ? { ...defaultOptions, ...options } : defaultOptions; | ||
} | ||
/** | ||
* @inheritdoc | ||
*/ | ||
duration() { | ||
return this.delay; | ||
next() { | ||
return instance(this.options).next(undefined); | ||
} | ||
} | ||
/** | ||
* An implementation of exponential backoff. | ||
*/ | ||
const instance = (options, state, delay = 0, attempt = -1) => ({ | ||
duration: delay, | ||
next() { | ||
if (this.attempt >= this.options.maxAttempts - 1) { | ||
if (attempt >= options.maxAttempts - 1) { | ||
return undefined; | ||
} | ||
const e = new ExponentialBackoff(this.options); | ||
[e.delay, e.state] = this.options.generator(this.state, this.options); | ||
e.attempt = this.attempt + 1; | ||
return e; | ||
} | ||
} | ||
const [nextDelay, nextState] = options.generator(state, options); | ||
return instance(options, nextState, nextDelay, attempt + 1); | ||
}, | ||
}); | ||
//# sourceMappingURL=ExponentialBackoff.js.map |
@@ -7,9 +7,9 @@ import { expectDurations } from './Backoff.test'; | ||
const b = new ExponentialBackoff({ generator: noJitterGenerator }); | ||
expectDurations(b, [0, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 30000, 30000]); | ||
expectDurations(b, [128, 256, 512, 1024, 2048, 4096, 8192, 16384, 30000, 30000, 30000]); | ||
}); | ||
it('sets max retries correctly', () => { | ||
const b = new ExponentialBackoff({ generator: noJitterGenerator, maxAttempts: 4 }); | ||
expectDurations(b, [0, 128, 256, 512, undefined]); | ||
expectDurations(b, [128, 256, 512, 1024, undefined]); | ||
}); | ||
}); | ||
//# sourceMappingURL=ExponentialBackoff.test.js.map |
@@ -1,17 +0,12 @@ | ||
import { IBackoff } from './Backoff'; | ||
import { IBackoff, IBackoffFactory } from './Backoff'; | ||
/** | ||
* Backoff that returns a number from an iterable. | ||
*/ | ||
export declare class IterableBackoff implements IBackoff<void> { | ||
export declare class IterableBackoff implements IBackoffFactory<unknown> { | ||
private readonly durations; | ||
private readonly index; | ||
constructor(durations: ReadonlyArray<number>, index?: number); | ||
constructor(durations: ReadonlyArray<number>); | ||
/** | ||
* @inheritdoc | ||
*/ | ||
duration(): number; | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next(): IterableBackoff | undefined; | ||
next(): IBackoff<unknown>; | ||
} |
@@ -5,8 +5,4 @@ /** | ||
export class IterableBackoff { | ||
constructor(durations, index = 0) { | ||
constructor(durations) { | ||
this.durations = durations; | ||
this.index = index; | ||
if (index >= durations.length) { | ||
throw new RangeError(`IterableBackoff index ${0} >= number of durations (${durations.length})`); | ||
} | ||
} | ||
@@ -16,14 +12,10 @@ /** | ||
*/ | ||
duration() { | ||
return this.durations[this.index]; | ||
} | ||
/** | ||
* @inheritdoc | ||
*/ | ||
next() { | ||
return this.index < this.durations.length - 1 | ||
? new IterableBackoff(this.durations, this.index + 1) | ||
: undefined; | ||
return instance(this.durations, 0); | ||
} | ||
} | ||
const instance = (durations, index) => ({ | ||
duration: durations[index], | ||
next: () => (index < durations.length - 1 ? instance(durations, index + 1) : undefined), | ||
}); | ||
//# sourceMappingURL=IterableBackoff.js.map |
@@ -1,2 +0,1 @@ | ||
import { expect } from 'chai'; | ||
import { expectDurations } from './Backoff.test'; | ||
@@ -9,6 +8,3 @@ import { IterableBackoff } from './IterableBackoff'; | ||
}); | ||
it('throws a range error if empty', () => { | ||
expect(() => new IterableBackoff([])).to.throw(RangeError); | ||
}); | ||
}); | ||
//# sourceMappingURL=IterableBackoff.test.js.map |
@@ -1,2 +0,2 @@ | ||
import { IBackoff, IExponentialBackoffOptions } from './backoff/Backoff'; | ||
import { IBackoff, IBackoffFactory, IExponentialBackoffOptions } from './backoff/Backoff'; | ||
import { DelegateBackoffFn } from './backoff/DelegateBackoff'; | ||
@@ -26,3 +26,3 @@ import { CancellationToken } from './CancellationToken'; | ||
export interface IRetryPolicyConfig { | ||
backoff?: IBackoff<IRetryBackoffContext<unknown>>; | ||
backoff?: IBackoffFactory<IRetryBackoffContext<unknown>>; | ||
/** | ||
@@ -29,0 +29,0 @@ * Whether to unreference the internal timer. This means the policy will not |
@@ -1,2 +0,2 @@ | ||
import { ExponentialBackoff } from './backoff/Backoff'; | ||
import { ExponentialBackoff, } from './backoff/Backoff'; | ||
import { CompositeBackoff } from './backoff/CompositeBackoff'; | ||
@@ -89,3 +89,4 @@ import { ConstantBackoff } from './backoff/ConstantBackoff'; | ||
async execute(fn, cancellationToken = CancellationToken.None) { | ||
let backoff = this.options.backoff || new ConstantBackoff(0, 1); | ||
const factory = this.options.backoff || new ConstantBackoff(0, 1); | ||
let backoff; | ||
for (let retries = 0;; retries++) { | ||
@@ -96,11 +97,19 @@ const result = await this.executor.invoke(fn, { attempt: retries, cancellationToken }); | ||
} | ||
if (backoff && !cancellationToken.isCancellationRequested) { | ||
const delayDuration = backoff.duration(); | ||
const delayPromise = delay(delayDuration, !!this.options.unref); | ||
// A little sneaky reordering here lets us use Sinon's fake timers | ||
// when we get an emission in our tests. | ||
this.onRetryEmitter.emit({ ...result, delay: delayDuration }); | ||
await delayPromise; | ||
backoff = backoff.next({ attempt: retries + 1, cancellationToken, result }); | ||
continue; | ||
if (!cancellationToken.isCancellationRequested) { | ||
const context = { attempt: retries + 1, cancellationToken, result }; | ||
if (retries === 0) { | ||
backoff = factory.next(context); | ||
} | ||
else if (backoff) { | ||
backoff = backoff.next(context); | ||
} | ||
if (backoff) { | ||
const delayDuration = backoff.duration; | ||
const delayPromise = delay(delayDuration, !!this.options.unref); | ||
// A little sneaky reordering here lets us use Sinon's fake timers | ||
// when we get an emission in our tests. | ||
this.onRetryEmitter.emit({ ...result, delay: delayDuration }); | ||
await delayPromise; | ||
continue; | ||
} | ||
} | ||
@@ -107,0 +116,0 @@ this.onGiveUpEmitter.emit(result); |
@@ -134,7 +134,7 @@ import { expect, use } from 'chai'; | ||
.attempts(3) | ||
.execute(({ cancellationToken: cancellation }) => { | ||
.execute(({ cancellationToken }) => { | ||
calls++; | ||
expect(cancellation.isCancellationRequested).to.be.false; | ||
expect(cancellationToken.isCancellationRequested).to.be.false; | ||
parent.cancel(); | ||
expect(cancellation.isCancellationRequested).to.be.true; | ||
expect(cancellationToken.isCancellationRequested).to.be.true; | ||
throw err; | ||
@@ -141,0 +141,0 @@ }, parent.token)).to.eventually.be.rejectedWith(err); |
@@ -1,2 +0,2 @@ | ||
import { IBackoff, IExponentialBackoffOptions } from './backoff/Backoff'; | ||
import { IBackoff, IBackoffFactory, IExponentialBackoffOptions } from './backoff/Backoff'; | ||
import { DelegateBackoffFn } from './backoff/DelegateBackoff'; | ||
@@ -26,3 +26,3 @@ import { CancellationToken } from './CancellationToken'; | ||
export interface IRetryPolicyConfig { | ||
backoff?: IBackoff<IRetryBackoffContext<unknown>>; | ||
backoff?: IBackoffFactory<IRetryBackoffContext<unknown>>; | ||
/** | ||
@@ -29,0 +29,0 @@ * Whether to unreference the internal timer. This means the policy will not |
@@ -91,3 +91,4 @@ "use strict"; | ||
async execute(fn, cancellationToken = CancellationToken_1.CancellationToken.None) { | ||
let backoff = this.options.backoff || new ConstantBackoff_1.ConstantBackoff(0, 1); | ||
const factory = this.options.backoff || new ConstantBackoff_1.ConstantBackoff(0, 1); | ||
let backoff; | ||
for (let retries = 0;; retries++) { | ||
@@ -98,11 +99,19 @@ const result = await this.executor.invoke(fn, { attempt: retries, cancellationToken }); | ||
} | ||
if (backoff && !cancellationToken.isCancellationRequested) { | ||
const delayDuration = backoff.duration(); | ||
const delayPromise = delay(delayDuration, !!this.options.unref); | ||
// A little sneaky reordering here lets us use Sinon's fake timers | ||
// when we get an emission in our tests. | ||
this.onRetryEmitter.emit({ ...result, delay: delayDuration }); | ||
await delayPromise; | ||
backoff = backoff.next({ attempt: retries + 1, cancellationToken, result }); | ||
continue; | ||
if (!cancellationToken.isCancellationRequested) { | ||
const context = { attempt: retries + 1, cancellationToken, result }; | ||
if (retries === 0) { | ||
backoff = factory.next(context); | ||
} | ||
else if (backoff) { | ||
backoff = backoff.next(context); | ||
} | ||
if (backoff) { | ||
const delayDuration = backoff.duration; | ||
const delayPromise = delay(delayDuration, !!this.options.unref); | ||
// A little sneaky reordering here lets us use Sinon's fake timers | ||
// when we get an emission in our tests. | ||
this.onRetryEmitter.emit({ ...result, delay: delayDuration }); | ||
await delayPromise; | ||
continue; | ||
} | ||
} | ||
@@ -109,0 +118,0 @@ this.onGiveUpEmitter.emit(result); |
@@ -136,7 +136,7 @@ "use strict"; | ||
.attempts(3) | ||
.execute(({ cancellationToken: cancellation }) => { | ||
.execute(({ cancellationToken }) => { | ||
calls++; | ||
chai_1.expect(cancellation.isCancellationRequested).to.be.false; | ||
chai_1.expect(cancellationToken.isCancellationRequested).to.be.false; | ||
parent.cancel(); | ||
chai_1.expect(cancellation.isCancellationRequested).to.be.true; | ||
chai_1.expect(cancellationToken.isCancellationRequested).to.be.true; | ||
throw err; | ||
@@ -143,0 +143,0 @@ }, parent.token)).to.eventually.be.rejectedWith(err); |
{ | ||
"name": "cockatiel", | ||
"version": "1.1.1", | ||
"version": "2.0.0", | ||
"description": "A resilience and transient-fault-handling library that allows developers to express policies such as Backoff, Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. Inspired by .NET Polly.", | ||
@@ -15,3 +15,3 @@ "main": "dist/index.js", | ||
"test:cover": "rimraf dist && tsc && nyc npm run test:unit", | ||
"test:unit": "mocha --opts mocha.opts", | ||
"test:unit": "mocha", | ||
"test:lint": "tslint -p tsconfig.json", | ||
@@ -18,0 +18,0 @@ "test:fmt": "prettier --list-different \"src/**/*.ts\" \"*.md\"", |
@@ -5,3 +5,3 @@ # Cockatiel | ||
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/cockatiel)](https://bundlephobia.com/result?p=cockatiel@0.1.0) | ||
![Libraries.io dependency status for latest release](https://david-dm.org/connor4312/cockatiel.svg) | ||
![No dependencies](https://img.shields.io/badge/dependencies-none-success) | ||
@@ -277,12 +277,7 @@ Cockatiel is resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback. .NET has [Polly](https://github.com/App-vNext/Polly), a wonderful one-stop shop for all your fault handling needs--I missed having such a library for my JavaScript projects, and grew tired of copy-pasting retry logic between my projects. Hence, this module! | ||
Backoff algorithms are immutable. They adhere to the interface: | ||
Backoff algorithms are immutable. The backoff class adheres to the interface: | ||
```ts | ||
export interface IBackoff<T> { | ||
export interface IBackoffFactory<T> { | ||
/** | ||
* Returns the number of milliseconds to wait for this backoff attempt. | ||
*/ | ||
duration(): number; | ||
/** | ||
* Returns the next backoff duration. Can return "undefined" to signal | ||
@@ -295,2 +290,15 @@ * that we should stop backing off. | ||
The backoff, returned from the `next()` call, has the appropriate delay and `next()` method again. | ||
```ts | ||
export interface IBackoff<T> { | ||
next(context: T): IBackoff<T> | undefined; // same as above | ||
/** | ||
* Returns the number of milliseconds to wait for this backoff attempt. | ||
*/ | ||
readonly duration: number; | ||
} | ||
``` | ||
### ConstantBackoff | ||
@@ -377,3 +385,3 @@ | ||
// Wait 100ms, 200ms, and then 500ms between attempts before giving up: | ||
const backoff new IterableBackoff([100, 200, 500]); | ||
const backoff = new IterableBackoff([100, 200, 500]); | ||
``` | ||
@@ -380,0 +388,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
520308
1125
7519