@creejs/commons-retrier
Version:
Common Utils About Task Retrying
1,270 lines (1,155 loc) • 51.5 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.CommonsLang = {}));
})(this, (function (exports) { 'use strict';
var t$1={constructorName:function(t){return t?.constructor?.name},defaults:function(t,...e){if(null==t)throw new TypeError('"target" must not be null or undefined');for(const n of e)if(null!=n)for(const e in n) void 0===t[e]&&(t[e]=n[e]);return t},extend:e$1,extends:e$1,equals:function(t,e){if(t===e)return true;if("function"==typeof t?.equals)return t.equals(e);if("function"==typeof e?.equals)return e.equals(t);return false},isBrowser:n$1,isNode:function(){return !n$1()}};function e$1(t,...e){if(null==t)throw new TypeError('"target" must not be null or undefined');for(const n of e)if(null!=n)for(const e in n)t[e]=n[e];return t}function n$1(){return "undefined"!=typeof window&&"undefined"!=typeof document}var r$1={isNumber:h$1,isNil:f$1};function s$1(t){return "function"==typeof t}function f$1(t){return null==t}function l$1(t){return !!h$1(t)&&t>0}function c$1(t){return !!h$1(t)&&t>=0}function h$1(t){return null!=t&&"number"==typeof t}function d$1(t){return null!=t&&"function"==typeof t.then}function m$1(t){return null!=t&&"string"==typeof t}var b$1={assertNumber:v$1,assertPositive:function(t,e){if(!l$1(t))throw new Error(`${e?'"'+e+'" ':" "}Not Positive: ${t}`)},assertNotNegative:function(t,e){if(!c$1(t))throw new Error(`${e?'"'+e+'" ':" "}Not "0 or Positive": ${t}`)},assertFunction:S,assertNotNil:function(t,e){if(f$1(t))throw new Error((e?'"'+e+'" ':" ")+"Should Not Nil")},assertString:O$1};function $(t,e){if(!Array.isArray(t))throw new Error(`${e?e+"":" "}Not Array: type=${typeof t} value=${JSON.stringify(t)}`)}function O$1(t,e){if(!m$1(t))throw new Error(`${e?'"'+e+'" ':" "}Not String: type=${typeof t} value=${JSON.stringify(t)}`)}function v$1(t,e){if(!h$1(t))throw new Error(`${e?'"'+e+'" ':" "}Not Number: type=${typeof t} value=${JSON.stringify(t)}`)}function S(t,e){if(!s$1(t))throw new Error(`${e?'"'+e+'" ':" "}Not Function: type=${typeof t} value=${JSON.stringify(t)}`)}function P(t,e){if(!d$1(t))throw new Error(`${e?'"'+e+'" ':" "}Not Promise: type=${typeof t} value=${JSON.stringify(t)}`)}var B={defer:U,delay:function(t,e){h$1(t)?(e=t,t=Promise.resolve()):null==t&&null==e&&(e=1,t=Promise.resolve());null!=t&&P(t),v$1(e=e??1e3);const n=U(),r=Date.now();return t.then((...t)=>{const o=Date.now()-r;o<e?setTimeout(()=>n.resolve(...t),e-o):n.resolve(...t);}).catch(t=>{const o=Date.now()-r;o<e?setTimeout(()=>n.reject(t),e-o):n.reject(t);}),n.promise},timeout:function(t,e,n){P(t),v$1(e=e??1);const r=U(e,n),o=Date.now();return t.then((...t)=>{Date.now()-o<=e?r.resolve(...t):r.reject(new Error(n??`Promise Timeout: ${e}ms`));}).catch(t=>{!r.resolved&&!r.rejected&&r.reject(t);}),r.promise},allSettled:C,returnValuePromised:I,series:async function(t){$(t);const e=[];for(const n of t)S(n),e.push(await n());return e},seriesAllSettled:async function(t){$(t);const e=[];for(const n of t){S(n);try{e.push({ok:!0,result:await n()});}catch(t){e.push({ok:false,result:t});}}return e},parallel:async function(t,e=5){if($(t),v$1(e),e<=0)throw new Error(`Invalid maxParallel: ${e}, should > 0`);t.forEach(t=>S(t));const n=[];if(t.length<=e){const e=await Promise.all(t.map(t=>I(t)));return n.push(...e),n}const r=[];for(const o of t)if(S(o),r.push(o),r.length>=e){const t=await Promise.all(r.map(t=>I(t)));n.push(...t),r.length=0;}if(r.length>0&&r.length<e){const t=await Promise.all(r.map(t=>I(t)));n.push(...t);}return n},parallelAllSettled:async function(t,e=5){if($(t),v$1(e),e<=0)throw new Error(`Invalid maxParallel: ${e}, should > 0`);t.forEach(t=>S(t));const n=[];if(t.length<=e){const e=await C(t.map(t=>I(t)));return n.push(...e),n}const r=[];for(const o of t)if(S(o),r.push(o),r.length>=e){const t=await C(r.map(t=>I(t)));n.push(...t),r.length=0;}if(r.length>0&&r.length<e){const t=await C(r.map(t=>I(t)));n.push(...t);}return n}};function U(t=-1,e){v$1(t);const n={};let r;return t>=0&&(n.timerHandler=r=setTimeout(()=>{clearTimeout(r),n.timerCleared=true,n.reject(new Error(e??`Promise Timeout: ${t}ms`));},t),n.timerHandler=r),n.promise=new Promise((t,e)=>{n.resolve=(...e)=>{null!=r&&(clearTimeout(r),n.timerCleared=true),n.resolved=true,t(...e);},n.reject=t=>{null!=r&&(clearTimeout(r),n.timerCleared=true),n.rejected=true,e(t);};}),n.promise.cancel=()=>{null!=r&&(clearTimeout(r),n.timerCleared=true),n.rejected=true,n.canceled=n.promise.canceled=true,n.reject(new Error("Cancelled"));},n}async function C(t){$(t);const e=await Promise.allSettled(t),n=[];for(const t of e)"fulfilled"===t.status&&n.push({ok:true,result:t.value}),"rejected"===t.status&&n.push({ok:false,result:t.reason});return n}function I(t){try{const e=t();return d$1(e)?e:Promise.resolve(e)}catch(t){return Promise.reject(t)}}
// module vars
const DefaultMinInterval = 50;
const DefaultMaxInterval = 30 * 1000; // 30s
const DefaultMaxRetries = 3;
// internal
// module vars
const { assertPositive: assertPositive$5, assertNotNegative: assertNotNegative$3 } = b$1;
const { isNumber } = r$1;
class Policy {
/**
* Creates a new Policy instance with specified retry bounds.
*/
constructor () {
this._min = DefaultMinInterval;
this._max = DefaultMaxInterval;
this._nextInterval = this._min;
this._jitter = 0;
}
get jitter () {
return this._jitter
}
set jitter (jitter) {
assertNotNegative$3(jitter, 'jitter');
this._jitter = jitter;
}
/**
* Copies settings to target policy.
* 1. range
* 2. nextInterval value
* @param {Policy} targetPolicy - The policy to modify.
*/
copyPolicySettingTo (targetPolicy) {
targetPolicy.range(this._min, this._max);
targetPolicy._nextInterval = this._nextInterval;
}
/**
* Sets a fixed interval retry policy.
}
/**
* Sets the minimum and maximum intervals for retries.
* @param {number} min - Minimum delay in milliseconds (must be positive and less than max)
* @param {number} max - Maximum delay in milliseconds (must be positive and greater than min)
* @returns {this} Returns the Retrier instance for chaining
* @throws {Error} If min is not less than max or if values are not positive
*/
range (min, max) {
assertPositive$5(min, 'min');
assertPositive$5(max, 'max');
if (min >= max) {
throw new Error('min must < max')
}
this._min = min;
if (this._nextInterval < this._min) {
this._nextInterval = this._min;
}
this._max = max;
if (this._nextInterval > this._max) {
this._nextInterval = this._max;
}
return this
}
/**
* Sets the minimum retry delay in milliseconds.
* 1. will change currentInterval to min
* @param {number} min - The minimum delay (must be positive and less than max).
* @returns {this} The retrier instance for chaining.
* @throws {Error} If min is not positive or is greater than/equal to max.
*/
min (min) {
assertPositive$5(min, 'min');
if (min >= this._max) {
throw new Error('min must < max')
}
this._min = min;
this._nextInterval = this._min;
return this
}
/**
* Sets the maximum retry retry delay in milliseconds.
* @param {number} max - The maximum delay (must be positive and greater than min).
* @throws {Error} If max is not greater than min.
* @returns {this} The retrier instance for chaining.
*/
max (max) {
assertPositive$5(max, 'max');
if (max <= this._min) {
throw new Error('max must > min')
}
this._max = max;
if (this._nextInterval > this._max) {
this._nextInterval = this._max;
}
return this
}
reset () {
this._nextInterval = this._min;
return this
}
/**
* Interval ms of next execution
* @param {number} retries current retry times
* @returns {number}
*/
generate (retries) {
const rtnVal = this._nextInterval;
this._increase(retries);
return rtnVal
}
/**
* @param {number} retries current retry times
* @returns {number}
*/
_increase (retries) {
const generated = this._next(retries);
if (!isNumber(generated)) {
throw new Error('Generated Next Interval Not Number')
}
const nextInterval = this._jitter <= 0 ? generated : generated + Math.floor(Math.random() * this._jitter);
if (nextInterval < this._min) {
return (this._nextInterval = this._min)
} else if (nextInterval > this._max) {
return (this._nextInterval = this._max)
}
return (this._nextInterval = nextInterval)
}
/**
* subclass should implement this method
* @param {number} retries current retry times
* @returns {number} The interval in milliseconds to wait before the next retry attempt.
* @protected
*/
_next (retries) {
throw new Error('Not Impled Yet')
}
}
var e={isFunction:t,isNil:s};function t(e){return "function"==typeof e}function s(e){return null==e}function n(e){return null!=e&&"string"==typeof e}var r={assertNumber:function(e,t){if(!function(e){return null!=e&&"number"==typeof e}(e))throw new Error(`${t?'"'+t+'" ':" "}Not Number: type=${typeof e} value=${JSON.stringify(e)}`)},assertFunction:function(e,s){if(!t(e))throw new Error(`${s?'"'+s+'" ':" "}Not Function: type=${typeof e} value=${JSON.stringify(e)}`)},assertNotNil:function(e,t){if(s(e))throw new Error((t?'"'+t+'" ':" ")+"Should Not Nil")},assertString:function(e,t){if(!n(e))throw new Error(`${t?'"'+t+'" ':" "}Not String: type=${typeof e} value=${JSON.stringify(e)}`)},assertStringOrSymbol:function(e,t){if(!n(e)&&!function(e){return null!=e&&"symbol"==typeof e}(e))throw new Error(`${t?'"'+t+'" ':" "}Not String or Symbol: type=${typeof e} value=${JSON.stringify(e)}`)}};const i="DOwner$#$",{assertFunction:l,assertNotNil:a}=r;class o{constructor(e,t,s=false){a(e,"event"),l(t,"callback"),this._event=e,this._callback=t,this._isOnce=!!s,this._owner=void 0;}set owner(e){this._owner=e;}get owner(){return this._owner===i?void 0:this._owner}get event(){return this._event}get isOnce(){return this._isOnce}isSameCallback(e){return this._callback===e}get callback(){return this._callback}invoke(...e){try{return this._callback(...e)}finally{if(this._isOnce)try{this._event._remove(this);}catch(e){console.warn(e);}}}listener(...e){return this.invoke(...e)}}const{isFunction:c,isNil:h}=e,{assertStringOrSymbol:u,assertFunction:_}=r;class f{static get DefaultOwner(){return i}constructor(e){u(e,"eventName"),this._name=e,this._callbacks=new Set,this._listeners=[],this._callback2Listeners=new Map,this._listener2Owner=new Map,this._owner2Listeners=new Map;}get name(){return this._name}isEmpty(){return 0===this._callbacks.size}rawListeners(){return [...this._listeners]}listenerCount(e){return null==e?this._listeners.length:this._callback2Listeners.get(e)?.size??0}callbacks(){return [...this.rawListeners().map(e=>e.callback)]}emit(...e){if(0===this._listeners.length)return false;for(const t of [...this._listeners])t.invoke(...e);return true}hasListener(e){return !!c(e)&&this._callbacks.has(e)}hasOwner(e){return !h(e)&&this._owner2Listeners.has(e)}addListener(e,t){return this._addListener(e,t,false,false)}prependListener(e,t){return this._addListener(e,t,false,true)}addOnceListener(e,t){return this._addListener(e,t,true,false)}prependOnceListener(e,t){return this._addListener(e,t,true,true)}_addListener(e,t,s,n){if(h(e))return false;_(e),this._callbacks.has(e)||this._callbacks.add(e),t=t??i;const r=new o(this,e,s);r.owner=t,n?this._listeners.unshift(r):this._listeners.push(r),this._listener2Owner.set(r,t);let l=this._callback2Listeners.get(e);null==l&&(l=new Set,this._callback2Listeners.set(e,l)),l.add(r);let a=this._owner2Listeners.get(t);return null==a&&(a=new Set,this._owner2Listeners.set(t,a)),a.add(r),true}removeListener(e){if(h(e))return false;if(!this._callbacks.has(e))return false;this._callbacks.delete(e);const t=this._callback2Listeners.get(e);if(null==t)return false;this._callback2Listeners.delete(e);for(const e of t){ -1!==this._listeners.indexOf(e)&&this._listeners.splice(this._listeners.indexOf(e),1);const t=this._listener2Owner.get(e);if(null==t)continue;this._listener2Owner.delete(e);const s=this._owner2Listeners.get(t);null!=s&&(s.delete(e),0===s.size&&this._owner2Listeners.delete(t));}return true}_remove(e){const t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1);const{callback:s}=e,n=this._callback2Listeners.get(s);null!=n&&(n.delete(e),0===n.size&&(this._callback2Listeners.delete(s),this._callbacks.delete(s)));const r=this._listener2Owner.get(e);if(null==r)return;this._listener2Owner.delete(e);const i=this._owner2Listeners.get(r);null!=i&&(i.delete(e),0===i.size&&this._owner2Listeners.delete(r));}removeAllListeners(e){if(h(e))return this._callbacks.clear(),this._listeners.length=0,this._callback2Listeners.clear(),this._listener2Owner.clear(),this._owner2Listeners.clear(),this;const t=this._owner2Listeners.get(e);if(null==t)return this;this._owner2Listeners.delete(e);for(const e of t){ -1!==this._listeners.indexOf(e)&&this._listeners.splice(this._listeners.indexOf(e),1),this._listener2Owner.delete(e);const{callback:t}=e,s=this._callback2Listeners.get(t);null!=s&&(s.delete(e),0===s.size&&(this._callback2Listeners.delete(t),this._callbacks.delete(t)));}return this}}const{isNil:m}=e,{assertString:d,assertFunction:L,assertNumber:w,assertStringOrSymbol:v,assertNotNil:g}=r,p=["on","once","addListener","prependListener","prependOnceListener","off","offAll","offOwner","removeAllListeners","removeListener","emit","setMaxListeners","getMaxListeners","hasOwner","listeners","listenerCount","eventNames","rawListeners"];let b=10;class O{static mixin(e){const t=new O;e.__emitter=t;for(const s of p){const n=t[s];e[s]=n.bind(t);}return e}static get defaultMaxListeners(){return b}static set defaultMaxListeners(e){w(e),b=e??10;}constructor(){this._name2Event=new Map,this._maxListeners=b;}addListener(e,t,s){return this.on(e,t,s)}prependListener(e,t,s){d(e),L(t),this._checkMaxListeners(e);return this._getOrCreateEvent(e).prependListener(t,s),this}prependOnceListener(e,t,s){d(e),L(t),this._checkMaxListeners(e);return this._getOrCreateEvent(e).prependOnceListener(t,s),this}emit(e,...t){const s=this._name2Event.get(e);return null!=s&&!s.isEmpty()&&(s.emit(...t),true)}eventNames(){return [...this._name2Event.keys()]}getMaxListeners(){return this._maxListeners}listenerCount(e,t){v(e,"eventName");const s=this._name2Event.get(e);return null==s||s.isEmpty()?0:s.listenerCount(t)}listeners(e){v(e,"eventName");const t=this._name2Event.get(e);return null==t||t.isEmpty()?[]:t.callbacks()}off(e,t){const s=this._name2Event.get(e);return null==s?this:(s.removeListener(t),s.isEmpty()?(this._name2Event.delete(e),this):this)}offAll(e,t){v(e,"eventName");const s=this._name2Event.get(e);return null==s?this:(s.removeAllListeners(t),s.isEmpty()?(this._name2Event.delete(e),this):this)}offOwner(e){g(e,"owner");const t=[...this._name2Event.values()];for(const s of t)s.removeAllListeners(e),s.isEmpty()&&this._name2Event.delete(s.name);return this}on(e,t,s){d(e),L(t),this._checkMaxListeners(e);return this._getOrCreateEvent(e).addListener(t,s),this}_checkMaxListeners(e){let t=0;0!==this._maxListeners&&this._maxListeners!==1/0&&(t=this.listenerCount(e))>=this._maxListeners&&console.warn(`maxlistenersexceededwarning: Possible EventEmitter memory leak detected. ${t} ${e} listeners added to [${this}]. Use emitter.setMaxListeners() to increase limit`);}once(e,t,s){d(e),L(t);return this._getOrCreateEvent(e).addOnceListener(t,s),this}rawListeners(e){return this._name2Event.get(e)?.rawListeners()||[]}removeAllListeners(e,t){return this.offAll(e,t)}removeListener(e,t){return this.off(e,t)}setMaxListeners(e){if(w(e),e<0)throw new RangeError("maxListeners must >=0");return this._maxListeners=e,this}_getOrCreateEvent(e){if(this._name2Event.has(e))return this._name2Event.get(e);const t=new f(e);return this._name2Event.set(e,t),t}hasOwner(e){if(m(e))return false;for(const t of this._name2Event.values())if(t.hasOwner(e))return true;return false}}
const Start = 'start'; // retry started
const Stop = 'stop'; // retry stopped
const Retry = 'retry'; // one retry began
const Success = 'success'; // one task running succeeded
const Failure = 'failure'; // one task ran failed
const Timeout = 'timeout'; // total timeout
const TaskTimeout = 'task-timeout'; // one task timed out
const Completed = 'complete'; // all retries completed
const MaxRetries = 'max-retries'; // Reach the max retries
var Event = {
Start,
Retry,
Success,
Failure,
Timeout,
TaskTimeout,
Stop,
Completed,
MaxRetries
};
// 3rd
// internal
// module vars
const { assertPositive: assertPositive$4 } = b$1;
class FixedIntervalPolicy extends Policy {
/**
* Creates a fixed interval retry policy with the specified interval.
* @param {number} interval - The fixed interval (in milliseconds) between retry attempts.
*/
constructor (interval) {
super();
assertPositive$4(interval, 'interval');
this._interval = interval;
}
set interval (interval) {
assertPositive$4(interval, 'interval');
this._interval = interval;
}
get interval () {
return this._interval
}
/**
* Interval ms of next execution
* @param {number} retries
* @returns {number}
* @throws {Error} Always throws "Not Implemented Yet" error.
*/
_next (retries) {
return this.interval
}
}
// 3rd
// internal
// module vars
const { assertPositive: assertPositive$3 } = b$1;
class FixedIncreasePolicy extends Policy {
/**
* each call to _next() increases the interval by "increasement".
* @param {number} increasement - The fixed interval (in milliseconds) between retry attempts.
*/
constructor (increasement) {
super();
assertPositive$3(increasement, 'increasement');
this._increasement = increasement;
}
set increasement (increasement) {
assertPositive$3(increasement, 'increasement');
this._increasement = increasement;
}
get increasement () {
return this._increasement
}
/**
* Interval ms of next execution
* @param {number} retries
* @returns {number}
*/
_next (retries) {
if (this._nextInterval >= this._max) {
return this._max
}
return this._nextInterval + this.increasement
}
}
// 3rd
// internal
// module vars
const { assertPositive: assertPositive$2 } = b$1;
class FactoreIncreasePolicy extends Policy {
/**
* each call to _next() increases the interval by lastInterval * factor
* @param {number} factor - the increasement factor, >= 1
*/
constructor (factor) {
super();
assertPositive$2(factor, 'factor');
if (factor < 1) {
throw new Error('factor must be >= 1')
}
this._factor = factor;
}
set factor (factor) {
assertPositive$2(factor, 'factor');
if (factor < 1) {
throw new Error('factor must be >= 1')
}
this._factor = factor;
}
get factor () {
return this._factor
}
/**
* Interval ms of next execution
* @param {number} retries
* @returns {number}
*/
_next (retries) {
if (this._nextInterval >= this._max) {
return this._max
}
return this._nextInterval * this.factor
}
}
// 3rd
// internal
// module vars
const { assertPositive: assertPositive$1 } = b$1;
class ShuttlePolicy extends Policy {
/**
* the inteval value shuttles between min and max
* @param {number} stepLength - the step length to change
*/
constructor (stepLength) {
super();
assertPositive$1(stepLength, 'stepLength');
this._stepLength = stepLength;
this.increasement = stepLength;
}
set stepLength (stepLength) {
assertPositive$1(stepLength, 'stepLength');
this._stepLength = stepLength;
this.increasement = stepLength;
}
get stepLength () {
return this._stepLength
}
/**
* Interval ms of next execution
* @param {number} retries
* @returns {number}
* @throws {Error} Always throws "Not Implemented Yet" error.
*/
_next (retries) {
const nextInterval = this._nextInterval + this.increasement;
if (nextInterval >= this._max) {
this.increasement = -this.stepLength;
return this._max
} else if (nextInterval <= this._min) {
this.increasement = this.stepLength;
return this._min
}
return nextInterval
}
}
// owned
/**
* @typedef {import('./retrier.js').default} Retrier
*/
// module vars
const { assertNotNil, assertFunction: assertFunction$1 } = b$1;
class Task {
/**
* Creates a new Task instance.
* @param {Retrier} retrier - The retrier instance.
* @param {Function} task - The function to be executed as the task.
*/
constructor (retrier, task) {
assertNotNil(retrier, 'retrier');
assertFunction$1(task, 'task');
this.retrier = retrier;
this.task = task;
this.result = undefined;
this.error = undefined;
}
get failed () {
return this.error != null
}
get succeeded () {
return this.error == null
}
/**
* Executes the task with the given retry parameters.
* 1. if execution throw error, keep error in this.error
* 2. if execution return value, keep it in this.result
* 3. always return Promise<void>
* @param {number} retries - The number of retries attempted so far.
* @param {number} latence - The current latency ms.
* @param {number} nextInterval - The next interval ms.
* @returns {Promise<void>} The result of the task execution.
*/
async execute (retries, latence, nextInterval) {
try {
this.result = await this.task(retries, latence, nextInterval);
this.error = undefined;
} catch (e) {
this.error = e;
}
}
dispose () {
// @ts-ignore
this.retrier = undefined;
}
}
// owned
/**
* @typedef {import('./retrier.js').default} Retrier
*/
class AlwaysTask extends Task {
/**
* Checks if the given task is an instance of AlwaysTask.
* @param {*} task - The task to check.
* @returns {boolean} True if the task is an instance of AlwaysTask, false otherwise.
*/
static isAlwaysTask (task) {
return task instanceof AlwaysTask
}
/**
* Creates an AlwaysTask instance.
* @param {Retrier} retrier - The retrier instance to use for retry logic
* @param {Function} task - The task function to execute
* @param {boolean} resetRetryPolicyAfterSuccess - Whether to reset retry policy after successful execution
*/
constructor (retrier, task, resetRetryPolicyAfterSuccess) {
super(retrier, task);
this.resetPolicy = resetRetryPolicyAfterSuccess;
}
/**
* Executes the task with the given retry parameters.
* @param {number} retries - The number of retries attempted so far.
* @param {number} latence - The current latency ms.
* @param {number} nextInterval - The next interval ms.
* @returns {Promise<*>} The result of the task execution.
*/
async execute (retries, latence, nextInterval) {
await super.execute(retries, latence, nextInterval);
if (this.succeeded && this.resetPolicy) {
this.retrier.resetRetryPolicy();
}
}
}
// internal
// module vars
const { assertNotNegative: assertNotNegative$2 } = b$1;
/**
* @class FixedBackoff
*/
class FixedBackoff extends FixedIntervalPolicy {
/**
* Creates a fixed backoff policy with optional jitter.
* @param {number} fixedInterval - The fixed interval between retries in milliseconds.
* @param {number} [jitter=500] - The maximum random jitter to add to the interval in milliseconds.
*/
constructor (fixedInterval, jitter = 500) {
super(fixedInterval);
assertNotNegative$2(jitter, 'jitter');
this._jitter = jitter ?? 500;
}
}
// internal
// module vars
const { assertNotNegative: assertNotNegative$1 } = b$1;
/**
* @class ExponentialBackoffPolicy
*/
class ExponentialBackoffPolicy extends FactoreIncreasePolicy {
/**
* Creates an exponential backoff policy with optional jitter.
* @param {number} [jitter=500] - Maximum jitter in milliseconds to add to backoff intervals.
*/
constructor (jitter = 500) {
super(2);
assertNotNegative$1(jitter, 'jitter');
this._jitter = jitter ?? 500;
}
}
// internal
// module vars
const { assertNotNegative } = b$1;
/**
* @class LinearBackoff
*/
class LinearBackoff extends FixedIncreasePolicy {
/**
* Creates a linear backoff policy with optional jitter.
* @param {number} increasement - The base increasement value for backoff.
* @param {number} [jitter=500] - The maximum jitter value to add to backoff (default: 500).
*/
constructor (increasement, jitter = 500) {
super(increasement);
assertNotNegative(jitter, 'jitter');
this._jitter = jitter ?? 500;
}
}
// internal
// module vars
const { assertPositive, assertString, assertFunction, assertNumber } = b$1;
const { isNil } = r$1;
const TaskTimoutFlag = '!#@%$&^*';
/**
* @extends EventEmitter
*/
class Retrier {
/**
* Creates a new Retrier instance with a fixed interval policy.
* @param {number} [fixedInterval=1000] - The fixed interval in milliseconds between retry attempts. Defaults to 1000ms if not provided.
*/
constructor (fixedInterval) {
O.mixin(this);
/**
* @type {Policy}
*/
this._policy = new FixedIntervalPolicy(fixedInterval ?? 1000);
this._maxRetries = DefaultMaxRetries;
this._currentRetries = 1;
/**
* Timetou for total operation
* @type {number}
*/
this._timeout = 120000; // 120s
/**
* Timetou for single task
*/
this._taskTimeout = 2000; // 20s
this._name = 'unamed'; // Retrier name
/**
* A Deferred Object as Singal to prevent Task concurrent start
* @type {{resolve:Function, reject:Function, promise: Promise<*>}|undefined}
*/
this._taskingFlag = undefined;
/**
* A Deferred Object as Singal to prevent Task concurrent stop
* @type {{resolve:Function, reject:Function, promise: Promise<*>}|undefined}
*/
this._breakFlag = undefined;
/**
* Reason for break
* @type {Error|undefined}
*/
this._breakReason = undefined;
}
get running () {
return !isNil(this._taskingFlag)
}
/**
* Sets the name of the retrier.
* @param {string} retrierName - The name to assign to the retrier.
* @returns {this} The retrier instance for chaining.
*/
name (retrierName) {
assertString(retrierName, 'retrierName');
this._name = retrierName;
return this
}
/**
* Sets the retry attempts to be infinite by setting max retries to maximum safe integer.
* @returns {Object} The retrier instance for chaining.
*/
infinite () {
this._maxRetries = Infinity;
return this
}
/**
* Sets the maximum number of retry attempts.
* @param {number} times - The maximum number of retries.
* @returns {this} The Retrier instance for chaining.
*/
times (times) {
return this.maxRetries(times)
}
/**
* Sets the maximum number of retry attempts.
* @param {number} maxRetries - The maximum number of retries (must be positive).
* @returns {this} The retrier instance for chaining.
*/
maxRetries (maxRetries) {
assertPositive(maxRetries, 'maxRetries');
this._maxRetries = maxRetries;
return this
}
/**
* Sets the minimum retry delay in milliseconds.
* @param {number} min - The minimum delay (must be positive and less than max).
* @returns {this} The retrier instance for chaining.
* @throws {Error} If min is not positive or is greater than/equal to max.
*/
min (min) {
this._policy.min(min);
return this
}
/**
* Sets the maximum retry retry delay in milliseconds.
* @param {number} max - The maximum delay (must be positive and greater than min).
* @throws {Error} If max is not greater than min.
* @returns {this} The retrier instance for chaining.
*/
max (max) {
this._policy.max(max);
return this
}
/**
* Sets the minimum and maximum intervals for retries.
* @param {number} min - Minimum delay in milliseconds (must be positive and less than max)
* @param {number} max - Maximum delay in milliseconds (must be positive and greater than min)
* @returns {Retrier} Returns the Retrier instance for chaining
* @throws {Error} If min is not less than max or if values are not positive
*/
range (min, max) {
this._policy.range(min, max);
return this
}
/**
* Sets a fixed interval retry policy.
* @param {number} fixedInterval - The fixed interval in milliseconds between retries.
* @returns {Retrier} The Retrier instance for chaining.
*/
fixedInterval (fixedInterval) {
const oldPolicy = this._policy;
if (oldPolicy instanceof FixedIntervalPolicy) {
oldPolicy.interval = fixedInterval;
return this
}
const newPolicy = new FixedIntervalPolicy(fixedInterval);
oldPolicy?.copyPolicySettingTo(newPolicy);
newPolicy.reset();
this._policy = newPolicy;
return this
}
/**
* sets a fixed backoff strategy.
* @param {number} fixedInterval - The fixed interval between retries in milliseconds.
* @param {number} [jitter=500] - The maximum jitter to add to the interval in milliseconds.
* @returns {Retrier} A retrier instance configured with fixed backoff.
*/
fixedBackoff (fixedInterval, jitter = 500) {
const oldPolicy = this._policy;
if (oldPolicy instanceof FixedIntervalPolicy) {
oldPolicy.interval = fixedInterval;
oldPolicy.jitter = jitter;
return this
}
const newPolicy = new FixedBackoff(fixedInterval, jitter);
oldPolicy?.copyPolicySettingTo(newPolicy);
newPolicy.reset();
this._policy = newPolicy;
return this
}
/**
* Sets a fixed increase policy for retry intervals.
* @param {number} increasement - The fixed amount to increase the interval by on each retry.
* @returns {this} The retrier instance for chaining.
*/
fixedIncrease (increasement) {
const oldPolicy = this._policy;
if (oldPolicy instanceof FixedIncreasePolicy) {
oldPolicy.increasement = increasement;
return this
}
const newPolicy = new FixedIncreasePolicy(increasement);
oldPolicy?.copyPolicySettingTo(newPolicy);
newPolicy.reset();
this._policy = newPolicy;
return this
}
/**
* Sets a fixed increase policy for retry intervals.
* @param {number} increasement - The fixed amount to increase the interval by on each retry.
* @param {number} [jitter=500] - The maximum jitter to add to the interval in milliseconds.
* @returns {this} The retrier instance for chaining.
*/
linearBackoff (increasement, jitter = 500) {
const oldPolicy = this._policy;
if (oldPolicy instanceof LinearBackoff) {
oldPolicy.increasement = increasement;
oldPolicy.jitter = jitter;
return this
}
const newPolicy = new LinearBackoff(increasement, jitter);
oldPolicy?.copyPolicySettingTo(newPolicy);
newPolicy.reset();
this._policy = newPolicy;
return this
}
/**
* Sets a fixed increase factor for retry delays.
* @param {number} factor - The multiplier for delay increase between retries.
* @returns {this} The retrier instance for method chaining.
*/
factorIncrease (factor) {
const oldPolicy = this._policy;
if (oldPolicy instanceof FactoreIncreasePolicy) {
oldPolicy.factor = factor;
return this
}
const newPolicy = new FactoreIncreasePolicy(factor);
oldPolicy?.copyPolicySettingTo(newPolicy);
newPolicy.reset();
this._policy = newPolicy;
return this
}
/**
* Creates a new Retrier instance with exponential-backoff strategy.
* @param {number} [jitter]
* @returns {Retrier} A new Retrier instance
*/
exponentialBackoff (jitter = 500) {
const oldPolicy = this._policy;
if (oldPolicy instanceof ExponentialBackoffPolicy) {
oldPolicy.jitter = jitter;
return this
}
const newPolicy = new ExponentialBackoffPolicy(jitter);
oldPolicy?.copyPolicySettingTo(newPolicy);
newPolicy.reset();
this._policy = newPolicy;
return this
}
/**
* Sets a shuttle retry policy with the given step length.
* @param {number} stepLength - The interval between retry attempts.
* @returns {this} The Retrier instance for chaining.
*/
shuttleInterval (stepLength) {
const oldPolicy = this._policy;
if (oldPolicy instanceof ShuttlePolicy) {
oldPolicy.stepLength = stepLength;
return this
}
const newPolicy = new ShuttlePolicy(stepLength);
oldPolicy?.copyPolicySettingTo(newPolicy);
newPolicy.reset();
this._policy = newPolicy;
return this
}
/**
* Sets the timeout duration for each Task execution.
* 1. must > 0
* @param {number} timeout - The timeout duration in milliseconds.
* @returns {Object} The retrier instance for chaining.
*/
taskTimeout (timeout) {
assertPositive(timeout, 'timeout');
this._taskTimeout = timeout;
return this
}
/**
* Sets the timeout duration for all retries.
* 1. <= 0 - no timeout
* 2. \> 0 - timeout duration in milliseconds
* @param {number} timeout - The timeout duration in milliseconds.
* @returns {Object} The retrier instance for chaining.
*/
timeout (timeout) {
assertNumber(timeout, 'timeout');
this._timeout = timeout;
return this
}
/**
* Sets the task function to be retried.
* @param {Function} task - The function to be executed and retried on failure.
* @returns {this} Returns the retrier instance for chaining.
*/
task (task) {
assertFunction(task, 'task');
this._task = new Task(this, task);
return this
}
/**
* alias of {@linkcode Retrier.task()}
* @param {Function} task - The function to be executed and retried
* @return {this}
*/
retry (task) {
this.task(task);
return this
}
/**
* Executes the given task, and never stop
* 1. if the task fails, will retry it after the interval generated by RetryPolicy
* 2. if the task succeeds, reset RetryPolicy to Minimum Interval and continue to run the task
* @param {Function} task - The async function to execute and retry.
* @param {boolean} [resetAfterSuccess=false] - Whether to reset retry counters after success.
* @returns {this} The Retrier instance for chaining.
*/
always (task, resetAfterSuccess = false) {
this._task = new AlwaysTask(this, task, resetAfterSuccess);
return this
}
/**
* Starts the retry process.
* @returns {Promise<*>}
*/
async start () {
if (this._task == null) {
throw new Error('No Task to Retry')
}
if (this._taskingFlag != null) {
return this._taskingFlag.promise
}
const startAt = Date.now();
let lastError = null;
// @ts-ignore
this.emit(Event.Start, startAt);
this._taskingFlag = B.defer();
let latency = null;
while (true) {
// need to stop?
if (this._breakFlag != null) {
this._taskingFlag.reject(this._breakReason);
break
}
latency = Date.now() - startAt;
// total timeout?
if (!isInfinite(this._timeout) && latency >= this._timeout) { // total timeout
// @ts-ignore
this.emit(Event.Timeout, this._currentRetries, latency, this._timeout);
// always task, treat as success, resolve the whole promise with <void>
if (AlwaysTask.isAlwaysTask(this._task)) {
this._taskingFlag.resolve();
break
}
this._taskingFlag.reject(lastError ?? new Error(`Timeout "${this._timeout}" Exceeded`));
break
}
// @ts-ignore
this.emit(Event.Retry, this._currentRetries, latency);
const task = this._task; // take task, it may be changed in events' callback functions
const nextDelay = this._policy.generate(this._currentRetries);
try {
try {
await B.timeout(task.execute(this._currentRetries, latency, nextDelay), this._taskTimeout, TaskTimoutFlag);
} catch (err) {
// @ts-ignore
if (err.message === TaskTimoutFlag) {
// @ts-ignore
this.emit(Event.TaskTimeout, this._currentRetries, latency, this._taskTimeout);
}
throw err
}
// @ts-ignore
if (task.failed) {
lastError = task.error;
throw task.error
}
const rtnVal = task.result;
// @ts-ignore
this.emit(Event.Success, rtnVal, this._currentRetries, latency);
// Not AwaysTask, we can finish all the retries with success
if (!AlwaysTask.isAlwaysTask(task)) {
this._taskingFlag.resolve(rtnVal);
break
}
// AwaysTask, continue to run the task
} catch (e) {
// @ts-ignore
this.emit(Event.Failure, e, this._currentRetries, latency);
}
const nextRetries = ++this._currentRetries;
// next retry, max retries reached?
if (this._currentRetries > this._maxRetries) {
// @ts-ignore
this.emit(Event.MaxRetries, nextRetries, this._maxRetries);
// always task, treat as success, resolve the whole promise with <void>
if (AlwaysTask.isAlwaysTask(task)) {
this._taskingFlag.resolve();
break
}
this._taskingFlag.reject(lastError ?? new Error(`Max Retries Exceeded, Retring ${this._currentRetries} times > max ${this._maxRetries}`));
break
}
await B.delay(nextDelay);
}
this._taskingFlag.promise.finally(() => {
this.resetRetryPolicy();
this._taskingFlag = undefined;
const spent = Date.now() - startAt;
// @ts-ignore
this.emit(Event.Completed, this._currentRetries, spent);
});
return this._taskingFlag.promise
}
/**
* Stops the retrier with an optional reason. If already stopping, returns the existing break promise.
* @param {Error} [reason] - Optional reason for stopping (defaults to 'Manually Stop' error).
* @returns {Promise<void>} A promise that resolves when the retrier has fully stopped.
*/
async stop (reason) {
// @ts-ignore
this.emit(Event.Stop, reason);
if (this._taskingFlag == null) {
return // no task running
}
if (this._breakFlag != null) {
// @ts-ignore
return this._breakFlag.promise
}
this._breakFlag = B.defer();
this._breakReason = reason ?? new Error('Manually Stop');
// @ts-ignore
this.once(Event.Completed, () => {
// @ts-ignore
this._breakFlag.resolve();
});
// @ts-ignore
this._breakFlag.promise.finally(() => {
this._breakFlag = undefined;
this._breakReason = undefined;
});
return this._breakFlag.promise
}
/**
* Resets the retry policy to its initial state.
*/
resetRetryPolicy () {
this._policy.reset();
}
/**
* Registers a listener function to be called on "retry" events.
* @param {Function} listener - The callback function
* @returns {this}
*/
onRetry (listener) {
// @ts-ignore
this.on(Event.Retry, listener);
return this
}
/**
* Registers a listener for "error" events.
* @param {Function} listener - The callback function
* @returns {this}
*/
onError (listener) {
// @ts-ignore
this.on(Event.Error, listener);
return this
}
/**
* Registers a listener for "failure" events.
* @param {Function} listener - The callback function
* @returns {this}
*/
onFailure (listener) {
// @ts-ignore
this.on(Event.Failure, listener);
return this
}
/**
* Registers a listener for "success" events.
* @param {Function} listener - The callback function
* @returns {this}
*/
onSuccess (listener) {
// @ts-ignore
this.on(Event.Success, listener);
return this
}
/**
* Registers a listener for "start" events.
* @param {Function} listener - The callback function
* @returns {this}
*/
onStart (listener) {
// @ts-ignore
this.on(Event.Start, listener);
return this
}
/**
* Registers a listener for "stop" events.
* @param {Function} listener - The callback function
* @returns {this}
*/
onStop (listener) {
// @ts-ignore
this.on(Event.Stop, listener);
return this
}
/**
* Registers a listener for "timeout" events.
* @param {Function} listener - The callback function
*/
onTimeout (listener) {
// @ts-ignore
this.on(Event.Timeout, listener);
return this
}
/**
* Registers a listener for "task-timeout" events.
* @param {Function} listener - The callback function
* @returns {this}
*/
onTaskTimeout (listener) {
// @ts-ignore
this.on(Event.TaskTimeout, listener);
return this
}
/**
* Registers a listener for "completed" events.
* @param {Function} listener - The callback function
*/
onCompleted (listener) {
// @ts-ignore
this.on(Event.Completed, listener);
return this
}
/**
* Registers a listener for the 'MaxRetries' event.
* @param {Function} listener - The callback function to be executed when max retries are reached.
* @returns {this}
*/
onMaxRetries (listener) {
// @ts-ignore
this.on(Event.MaxRetries, listener);
return this
}
}
/**
* Checks if a value represents infinity or a non-positive number.
* @param {number} value - The value to check
* @returns {boolean} True if the value is <= 0 or Infinity, false otherwise
*/
function isInfinite (value) {
return value <= 0 || value === Infinity
}
// owned
/**
* Creates a new Retrier instance with the specified name.
* @param {string} name - The name to assign to the retrier.
* @returns {Retrier} A new Retrier instance with the given name.
*/
function name (name) {
const retrier = new Retrier();
retrier.name(name);
return retrier
}
/**
* Creates and returns a Retrier instance configured for infinite retries.
* @returns {Retrier} A Retrier instance with infinite retry behavior.
*/
function infinite () {
const retrier = new Retrier();
retrier.infinite();
return retrier
}
/**
* Creates a retrier configured to attempt an operation a specified number of times.
* @param {number} times - The maximum number of retry attempts.
* @returns {Retrier} A configured Retrier instance with the specified max retries.
*/
function times (times) {
const retrier = new Retrier();
retrier.times(times);
return retrier
}
/**
* Alias for times.
* @param {number} maxRetries - The maximum number of retry attempts.
* @returns {Retrier} A configured Retrier instance with the specified max retries.
*/
function maxRetries (maxRetries) {
const retrier = new Retrier();
retrier.maxRetries(maxRetries);
return retrier
}
/**
* Sets the minimum Interval for the retrier and returns the instance.
* @param {number} min - The minimum Interval in milliseconds.
* @returns {Retrier} The retrier instance with updated minimum Interval.
*/
function min (min) {
const retrier = new Retrier();
retrier.min(min);
return retrier
}
/**
* Sets the maximum Interval for the retrier and returns the instance.
* @param {number} max - The maximum Interval in milliseconds.
* @returns {Retrier} The retrier instance with updated maximum Interval.
*/
function max (max) {
const retrier = new Retrier();
retrier.max(max);
return retrier
}
/**
* Creates a retrier with the specified Interval range.
* @param {number} min - Minimum Interval.
* @param {number} max - Maximum Interval.
* @returns {Retrier} A new Retrier instance configured with specified Interval range.
*/
function range (min, max) {
const retrier = new Retrier();
retrier.range(min, max);
return retrier
}
/**
* Creates a retrier with a fixed interval between attempts.
* @param {number} fixedInterval - The fixed interval in milliseconds between retry attempts.
* @returns {Retrier} A new Retrier instance configured with the specified fixed interval.
*/
function fixedInterval (fixedInterval) {
const retrier = new Retrier();
retrier.fixedInterval(fixedInterval);
return retrier
}
/**
* Creates a retrier with a fixed backoff strategy.
* @param {number} fixedInterval - The fixed interval between retries in milliseconds.
* @param {number} [jitter=500] - The maximum jitter to add to the interval in milliseconds.
* @returns {Retrier} A retrier instance configured with fixed backoff.
*/
function fixedBackoff (fixedInterval, jitter = 500) {
const retrier = new Retrier();
retrier.fixedBackoff(fixedInterval, jitter);
return retrier
}
/**
* Creates a retrier with a fixed increase strategy.
* @param {number} increasement - The fixed amount to increase on each retry.
* @returns {Retrier} A retrier instance configured with fixed increase.
*/
function fixedIncrease (increasement) {
const retrier = new Retrier();
retrier.fixedIncrease(increasement);
return retrier
}
/**
* Creates a retrier with a fixed increase strategy.
* @param {number} increasement - The fixed amount to increase on each retry.
* @param {number} [jitter=500] - The maximum jitter to add to the interval in milliseconds.
* @returns {Retrier} A retrier instance configured with fixed increase.
*/
function linearBackoff (increasement, jitter = 500) {
const retrier = new Retrier();
retrier.linearBackoff(increasement, jitter);
return retrier
}
/**
* Creates a new Retrier instance with factor-increase strategy.
* @param {number} factor - The factor by which to increase the interval on each retry.
* @returns {Retrier} A new Retrier instance with factor-increase strategy.
*/
function factorIncrease (factor) {
const retrier = new Retrier();
retrier.factorIncrease(factor);
return retrier
}
/**
* Creates a new Retrier instance with exponential-backoff strategy.
* @param {number} [jitter=500]
* @returns {Retrier} A new Retrier instance
*/
function exponentialBackoff (jitter = 500) {
const retrier = new Retrier();
retrier.exponentialBackoff(jitter);
return retrier
}
/**
* Creates a Retrier instance with a shuttle-interval strategt.
* @param {number} stepLength - The interval step length of each change
* @returns {Retrier} A configured Retrier instance with shuttle-interval strategt.
*/
function shuttleInterval (stepLength) {
const retrier = new Retrier();
retrier.shuttleInterval(stepLength);
return retrier
}
/**
* Creates a Retrier instance with a total-opertion timeout.
* @param {number} timeout - The timeout value in milliseconds.
* @returns {Retrier} A Retrier instance configured with the given timeout.
*/
function timeout (timeout) {
const retrier = new Retrier();
retrier.timeout(timeout);
return retrier
}
/**
* Creates a retrier instance with a single-task timeout.
* @param {number} timeout - The timeout duration in milliseconds for the retrier task.
* @returns {Retrier} A Retrier instance configured with the given task timeout.
*/
function taskTimeout (timeout) {
const retrier = new Retrier();
retrier.taskTimeout(timeout);
return retrier
}
/**
* Creates a new Retrier instance, sets the task to be retried, and starts the retry process.
* @param {Function} task - The asynchronous task function to be retried.
* @returns {Promise<*>} A promise that resolves when the retry process completes.
*/
function start (task) {
const retrier = new Retrier();
retrier.task(task);
return retrier.start()
}
// default export
var RetrierFactory = {
name,
infinite,
times,
maxRetries,
min,
m