attemptify
Version:
TypeScript retry library with no dependencies.
302 lines (284 loc) • 8.76 kB
text/typescript
import {Duration} from './duration';
import {RetryEventOnFailed} from './event/retry-event-on-failed';
import {RetryEventOnSuccess} from './event/retry-event-on-success';
import {ExhaustedRetryException} from './exception/exhausted-retey-exception';
import {RetryEventLister} from './listener/retry-event-lister';
import {RetryPolicy} from './policy/retry-policy';
import {RetryContext} from './retry-context';
/**
* Attempt handler.
* This class organizes a retry context and policy so that handles some attempts
*/
export class Attempt {
private readonly retryContext: RetryContext;
/**
* @param {RetryPolicy} retryPolicy
*/
constructor(
private retryPolicy: RetryPolicy,
) {
this.retryContext = new RetryContext();
}
private requireDebugLogging = false;
private retryEventListeners: RetryEventLister[] = [];
/**
* Add retry event listener to this attempt, so that
* listening each events
* @param {RetryEventLister} retryEventListener
* @return {Attempt}
*/
addListener(retryEventListener: RetryEventLister): Attempt {
this.addListeners([retryEventListener]);
return this;
}
/**
* Add some retry event listeners to this attempt.
* This stacks listeners, so those subscribe events by popping.
* @param {RetryEventLister} retryEventListeners
* @return {Attempt} return `this`
*/
addListeners(retryEventListeners: RetryEventLister[]): Attempt {
retryEventListeners.forEach((listener) =>
this.retryEventListeners.push(listener),
);
return this;
}
/**
* Return true if this attempt has one or more listeners.
* @return {boolean}
*/
hasListener(): boolean {
return this.retryEventListeners.length > 0;
}
/**
* Enable this attempt to debug-log
* @return {Attempt}
*/
enableDebugLogging(): Attempt {
this.requireDebugLogging = true;
return this;
}
/**
* Executing `producer` until exhausting along with `RetryPolicy`.
* @param {Function} producer
* @return {Promise<T>} Promise
* @throws {@link ExhaustedRetryException} when exhausting `producer`
*/
async executeAsync<T>(producer: () => Promise<T>): Promise<T> {
return this.doOnRetryAsync(producer);
}
/**
* Executing `producer` until exhausting along with `RetryPolicy`.
* After exhausted, try to evaluate `another` function.
* @param {Function} producer
* @param {Function} another
* @return {Promise<T>}
*/
async executeAsyncOrElse<T>(
producer: () => Promise<T>,
another: () => Promise<T>,
): Promise<T> {
return this.doOnRetryAsync(producer, another);
}
/**
* Executing `producer` until exhausting along with `RetryPolicy`.
* After exhausted, return default value.
* @param {Function} producer
* @param {Promise<T>} defaultValue
* @return {Promise<T>}
*/
async executeAsyncOrDefault<T>(
producer: () => Promise<T>,
defaultValue: T,
): Promise<T> {
return this.doOnRetryAsync(producer, () => {
return new Promise((resolve) => resolve(defaultValue));
});
}
/**
* Trys attempting and handles producers.
* If `another` is passed, exhausting attempts, execute `another`.
* @param {Function} producer
* @param {Function} another
* @return {Promise<T>}
*/
private async doOnRetryAsync<T>(
producer: () => Promise<T>,
another?: () => Promise<T>,
): Promise<T> {
while (this.retryPolicy.canRetry(this.retryContext)) {
try {
const result = await producer();
this.notifyRetryEventOnSuccess(
this.retryEventListeners,
this.retryContext,
);
return result;
} catch (e) {
if (this.retryPolicy.shouldNotRetry(e)) {
this.logDebugIfRequire(
this.requireDebugLogging,
`Not retry for the error caught: ${e}`,
);
continue;
}
this.retryContext.updateLastError(e);
const delay = this.retryPolicy.getNextDelay();
this.logDebugIfRequire(
this.requireDebugLogging,
`Attempt failed; count => ${this.retryContext.attemptsCount}`,
);
this.logDebugIfRequire(
this.requireDebugLogging,
`next waiting ===> ${delay.toMilliSecconds()}`,
);
this.notifyRetryEventOnFailed(
this.retryEventListeners,
this.retryContext,
);
await this.wait(delay);
}
}
if (another) {
return await another();
}
throw new ExhaustedRetryException('Attempt exhaustetd.');
}
/**
*
* @param {boolean} requireDebugLogging
* @param {string} message
*/
private logDebugIfRequire(
requireDebugLogging: boolean,
message: string,
): void {
if (requireDebugLogging) {
console.log(message);
}
}
/**
* Executing `producer` until exhausting along with `RetryPolicy`.
* @param {Function} producer
* @return {Promise<T>}
* @throws {@link ExhaustedRetryException} when exhausting `producer`.
*/
execute<T>(producer: () => T): T {
return this.doOnRetry(producer);
}
/**
* Executing `producer` until exhausting along with `RetryPolicy`.
* @param {Function} producer
* @param {Function} another
* @return {Promise<T>}
*/
executeOrElse<T>(producer: () => T, another?: () => T): T {
return this.doOnRetry(producer, another);
}
/**
* Executing `producer` until exhausting along with `RetryPolicy`.
* Return a result of `producer`, or default value when exhausted.
* @param {Function} producer Producer function
* @param {T} defaultValue Another result when attempt failed
* @return {T}
*/
executeOrDefault<T>(producer: () => T, defaultValue: T): T {
return this.doOnRetry(producer, () => {
return defaultValue;
});
}
/**
* Execute and handle error with retrying.
* @param {Function} producer
* @param {Function} another
* @return {T}
*/
private doOnRetry<T>(producer: () => T, another?: () => T): T {
while (this.retryPolicy.canRetry(this.retryContext)) {
try {
const result = producer();
this.notifyRetryEventOnSuccess(
this.retryEventListeners,
this.retryContext,
);
return result;
} catch (e) {
if (this.retryPolicy.shouldNotRetry(e)) {
this.logDebugIfRequire(
this.requireDebugLogging,
`Not retry catching error [${e.name}]`,
);
break;
}
this.logDebugIfRequire(
this.requireDebugLogging,
`Attempt failed; count => ${this.retryContext.attemptsCount}`,
);
this.retryContext.updateLastError(e);
this.notifyRetryEventOnFailed(
this.retryEventListeners,
this.retryContext,
);
}
const delay = this.retryPolicy.getNextDelay();
this.wait(delay)
.then((_) => _)
.catch((e) => {
this.logDebugIfRequire(
this.requireDebugLogging,
`Error occurred on waiting: ${e}`,
);
console.error(`Error occurred on waiting: ${e}`);
});
}
if (another) {
return another();
}
throw new ExhaustedRetryException('Attempt exhaustetd.');
}
/**
* Notify all listeners of a retry event on success.
* {@link RetryEventOnSuccess} of message to notify is created from
* {@link RetryContext}.
* @param {RetryEventLister[]} retryEventListeners
* @param {RetryContext} retryContext
*/
private notifyRetryEventOnSuccess(
retryEventListeners: RetryEventLister[],
retryContext: RetryContext,
): void {
const retryEvent = new RetryEventOnSuccess(retryContext.attemptsCount);
retryEventListeners.forEach((listener) => {
listener.onSuccess(retryEvent);
});
}
/**
* Notify all listeners of a retry event on error.
* {@link RetryEventOnFailed} of message to notify is created from
* {@link RetryContext}.
* @param {RetryEventLister[]} retryEventListeners
* @param {RetryContext} retryContext
*/
private notifyRetryEventOnFailed(
retryEventListeners: RetryEventLister[],
retryContext: RetryContext,
): void {
const retryEvent = new RetryEventOnFailed(
retryContext.attemptsCount,
retryContext.lastError,
);
retryEventListeners.forEach((listener) => {
listener.onFailed(retryEvent);
});
}
/**
* Wait seconds.
* @param {Duration} duration
* @return {Promise<void>}
*/
private async wait(duration: Duration): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, duration.toMilliSecconds());
});
}
}