@creejs/commons-retrier 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # @creejs/commons-retrier
2
+
3
+ Commons-retrier Library provides a Retrier to:
4
+
5
+ - **Retry a task until it matches your want**
6
+
7
+ - **Options**:
8
+
9
+ - **Times**
10
+
11
+ The max retires, how many times can we try?
12
+
13
+ - **Intervals**
14
+
15
+ After a retrying, what delay should we waiting for?
16
+
17
+ - **Minimum Interval**
18
+ - **Maximum Interval**
19
+ - **Change Policy**
20
+ - **Fixed Interval Policy**
21
+ - **Fixed Increase Policy**
22
+ - **Factor Increase Policy**
23
+ - **Shuttle Policy**
24
+
25
+ - **Timeout**
26
+
27
+ After the "timeout", whole retries will failed with last error
28
+
29
+ - **TaskTimeout**
30
+
31
+ Timeout of one Task execution
32
+
33
+ And it supports two types of usage:
34
+
35
+ 1. **retry(task)**
36
+
37
+ Run the task at intervals, until the task succeeds.
38
+
39
+ 2. **always(task)**
40
+
41
+ Always run the task again and again, despit it fails or succeeds, until reaching the maxRetries, or total timeout
42
+
43
+ ## Install
44
+
45
+ ```shell
46
+ npm intall @creejs/commons-retrier
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ 1. retry(task)
52
+
53
+ ```javascript
54
+ 'use strict'
55
+ const { Retrier } = require('@creejs/commons-retrier')
56
+
57
+ // Chainable API to create and initialize Retrier
58
+ const retrier = Retrier.infinite() // no retry limit
59
+ .min(100) // min interval, 100ms
60
+ .max(300) // max interval, 300ms
61
+ .fixedIncrease(20) // each call, increase interval by 20ms
62
+
63
+ // Task function to be retried
64
+ const task = (retries, latency) => {
65
+ if (retries <= 3) { // failed 3 times
66
+ throw new Error('Task failed') // fails, if throw error, or reject promise
67
+ }
68
+ return 'success' // succeed at 4th try
69
+ }
70
+ // will end retries when first successfully call happens
71
+ (async () => {
72
+ try {
73
+ const rtnVal = await retrier.task(task).start()
74
+ console.log(rtnVal)
75
+ // --> success
76
+ } catch (err) {
77
+ console.log(err)
78
+ }
79
+ })()
80
+ ```
81
+
82
+
83
+
84
+ 2. always(task)
85
+
86
+ ```javas
87
+ 'use strict'
88
+
89
+ const { Retrier } = require('@creejs/commons-retrier')
90
+
91
+ // Chainable API to create and initialize Retrier
92
+ const retrier = Retrier.times(4) // max retry 4 times
93
+ .min(100) // min interval, 100ms
94
+ .max(300) // max interval, 300ms
95
+ .fixedIncrease(20) // each call, increase interval by 20ms
96
+ .onSuccess((taskResult, retries, latency) => {
97
+ console.log(taskResult)
98
+ })
99
+ .onFailure((err, retries, latency) => {
100
+ console.error(err.message)
101
+ })
102
+ .onMaxRetries((nextRetries, maxRetries) => {
103
+ console.error(`Max retries reached, ${nextRetries} > maxRetries ${maxRetries}`)
104
+ })
105
+
106
+ // Task function to be retried
107
+ const task = (retries, latency) => {
108
+ // succeed at 1, 2 try
109
+ if (retries <= 2) {
110
+ return `Latency ${latency}ms, Task succeeded at ${retries} try`
111
+ }
112
+ // fails at 3, 4 try, if throw error, or reject promise
113
+ throw new Error(`Latency ${latency}ms, Task failed at ${retries} try`)
114
+ }
115
+
116
+ // will end retries when reach max retries
117
+ (async () => {
118
+ try {
119
+ await retrier.always(task).start()
120
+
121
+ console.log('Finished All Retries')
122
+ // --> Latency 0ms, Task succeeded at 1 try
123
+ // --> 105ms, Task succeeded at 2 try
124
+ // --> Latency 228ms, Task failed at 3 try
125
+ // --> Latency 370ms, Task failed at 4 try
126
+ // --> Max retries reached, 5 > maxRetries 4
127
+ // --> Finished All Retries
128
+ } catch (err) {
129
+ console.log(err)
130
+ }
131
+ })()
132
+ ```
133
+
134
+
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ module.exports = require('./lib')
@@ -0,0 +1,44 @@
1
+ 'use strict'
2
+
3
+ // owned
4
+ // eslint-disable-next-line no-unused-vars
5
+ const Retrier = require('./retrier')
6
+ const Task = require('./task')
7
+
8
+ class AlwaysTask extends Task {
9
+ /**
10
+ * Checks if the given task is an instance of AlwaysTask.
11
+ * @param {*} task - The task to check.
12
+ * @returns {boolean} True if the task is an instance of AlwaysTask, false otherwise.
13
+ */
14
+ static isAlwaysTask (task) {
15
+ return task instanceof AlwaysTask
16
+ }
17
+
18
+ /**
19
+ * Creates an AlwaysTask instance.
20
+ * @param {Retrier} retrier - The retrier instance to use for retry logic
21
+ * @param {Function} task - The task function to execute
22
+ * @param {boolean} resetRetryPolicyAfterSuccess - Whether to reset retry policy after successful execution
23
+ */
24
+ constructor (retrier, task, resetRetryPolicyAfterSuccess) {
25
+ super(retrier, task)
26
+ this.resetPolicy = resetRetryPolicyAfterSuccess
27
+ }
28
+
29
+ /**
30
+ * Executes the task with the given retry parameters.
31
+ * @param {number} retries - The number of retries attempted so far.
32
+ * @param {number} latence - The current latency ms.
33
+ * @param {number} nextInterval - The next interval ms.
34
+ * @returns {Promise<*>} The result of the task execution.
35
+ */
36
+ async execute (retries, latence, nextInterval) {
37
+ await super.execute(retries, latence, nextInterval)
38
+ if (this.succeeded && this.resetPolicy) {
39
+ this.retrier.resetRetryPolicy()
40
+ }
41
+ }
42
+ }
43
+
44
+ module.exports = AlwaysTask
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+ // module vars
3
+ const DefaultMinInterval = 50
4
+ const DefaultMaxInterval = 30 * 1000 // 30s
5
+
6
+ const DefaultMaxRetries = 3
7
+ module.exports = {
8
+ DefaultMinInterval,
9
+ DefaultMaxInterval,
10
+ DefaultMaxRetries
11
+ }
package/lib/event.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+ const Start = 'start' // retry started
3
+ const Stop = 'stop' // retry stopped
4
+ const Retry = 'retry' // one retry began
5
+ const Success = 'success' // one task running succeeded
6
+ const Failure = 'failure' // one task ran failed
7
+ const Timeout = 'timeout' // total timeout
8
+ const TaskTimeout = 'task-timeout' // one task timed out
9
+ const Completed = 'complete' // all retries completed
10
+ const MaxRetries = 'max-retries' // Reach the max retries
11
+
12
+ module.exports = {
13
+ Start,
14
+ Retry,
15
+ Success,
16
+ Failure,
17
+ Timeout,
18
+ TaskTimeout,
19
+ Stop,
20
+ Completed,
21
+ MaxRetries
22
+ }
package/lib/index.js ADDED
@@ -0,0 +1,28 @@
1
+ 'use strict'
2
+ // 3rd
3
+ const { LangUtils } = require('@creejs/commons-lang')
4
+
5
+ // owned
6
+ const Policy = require('./policy')
7
+ const Retrier = require('./retrier')
8
+ const Event = require('./event')
9
+ const RetrierFactory = require('./retrier-factory')
10
+
11
+ /**
12
+ * Add all factory methods to Retrier as static methods.
13
+ * Now we can create do something like this:
14
+ * ```
15
+ * Retrier.name('myRetrier')
16
+ * Retrier.infinite()
17
+ * ...
18
+ * ```
19
+ */
20
+ LangUtils.defaults(Retrier, RetrierFactory)
21
+
22
+ module.exports = {
23
+ Policy,
24
+ Retrier,
25
+ Event,
26
+ RetrierFactory,
27
+ ...RetrierFactory
28
+ }
@@ -0,0 +1,46 @@
1
+ 'use strict'
2
+ // 3rd
3
+ // internal
4
+ const { TypeAssert: { assertPositive } } = require('@creejs/commons-lang')
5
+ // owned
6
+ const Policy = require('../policy')
7
+
8
+ class FactoreIncreasePolicy extends Policy {
9
+ /**
10
+ * each call to _next() increases the interval by lastInterval * factor
11
+ * @param {number} factor - the increasement factor, >= 1
12
+ */
13
+ constructor (factor) {
14
+ super()
15
+ assertPositive(factor, 'factor')
16
+ if (factor < 1) {
17
+ throw new Error('factor must be >= 1')
18
+ }
19
+ this._factor = factor
20
+ }
21
+
22
+ set factor (factor) {
23
+ assertPositive(factor, 'factor')
24
+ if (factor < 1) {
25
+ throw new Error('factor must be >= 1')
26
+ }
27
+ this._factor = factor
28
+ }
29
+
30
+ get factor () {
31
+ return this._factor
32
+ }
33
+
34
+ /**
35
+ * Interval ms of next execution
36
+ * @returns {number}
37
+ */
38
+ _next () {
39
+ if (this._nextInterval >= this._max) {
40
+ return this._max
41
+ }
42
+ return this._nextInterval * this.factor
43
+ }
44
+ }
45
+
46
+ module.exports = FactoreIncreasePolicy
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+ // 3rd
3
+ // internal
4
+ const { TypeAssert: { assertPositive } } = require('@creejs/commons-lang')
5
+ // owned
6
+ const Policy = require('../policy')
7
+
8
+ class FixedIncreasePolicy extends Policy {
9
+ /**
10
+ * each call to _next() increases the interval by "increasement".
11
+ * @param {number} increasement - The fixed interval (in milliseconds) between retry attempts.
12
+ */
13
+ constructor (increasement) {
14
+ super()
15
+ assertPositive(increasement, 'increasement')
16
+ this._increasement = increasement
17
+ }
18
+
19
+ set increasement (increasement) {
20
+ assertPositive(increasement, 'increasement')
21
+ this._increasement = increasement
22
+ }
23
+
24
+ get increasement () {
25
+ return this._increasement
26
+ }
27
+
28
+ /**
29
+ * Interval ms of next execution
30
+ * @returns {number}
31
+ */
32
+ _next () {
33
+ if (this._nextInterval >= this._max) {
34
+ return this._max
35
+ }
36
+ return this._nextInterval + this.increasement
37
+ }
38
+ }
39
+
40
+ module.exports = FixedIncreasePolicy
@@ -0,0 +1,38 @@
1
+ 'use strict'
2
+ // 3rd
3
+ // internal
4
+ const { TypeAssert: { assertPositive } } = require('@creejs/commons-lang')
5
+ // owned
6
+ const Policy = require('../policy')
7
+
8
+ class FixedIntervalPolicy extends Policy {
9
+ /**
10
+ * Creates a fixed interval retry policy with the specified interval.
11
+ * @param {number} interval - The fixed interval (in milliseconds) between retry attempts.
12
+ */
13
+ constructor (interval) {
14
+ super()
15
+ assertPositive(interval, 'interval')
16
+ this._interval = interval
17
+ }
18
+
19
+ set interval (interval) {
20
+ assertPositive(interval, 'interval')
21
+ this._interval = interval
22
+ }
23
+
24
+ get interval () {
25
+ return this._interval
26
+ }
27
+
28
+ /**
29
+ * Interval ms of next execution
30
+ * @returns {number}
31
+ * @throws {Error} Always throws "Not Implemented Yet" error.
32
+ */
33
+ _next () {
34
+ return this.interval
35
+ }
36
+ }
37
+
38
+ module.exports = FixedIntervalPolicy
@@ -0,0 +1,49 @@
1
+ 'use strict'
2
+ // 3rd
3
+ // internal
4
+ const { TypeAssert: { assertPositive } } = require('@creejs/commons-lang')
5
+
6
+ // owned
7
+ const Policy = require('../policy')
8
+
9
+ class ShuttlePolicy extends Policy {
10
+ /**
11
+ * the inteval value shuttles between min and max
12
+ * @param {number} stepLength - the step length to change
13
+ */
14
+ constructor (stepLength) {
15
+ super()
16
+ assertPositive(stepLength, 'stepLength')
17
+ this._stepLength = stepLength
18
+ this.increasement = stepLength
19
+ }
20
+
21
+ set stepLength (stepLength) {
22
+ assertPositive(stepLength, 'stepLength')
23
+ this._stepLength = stepLength
24
+ this.increasement = stepLength
25
+ }
26
+
27
+ get stepLength () {
28
+ return this._stepLength
29
+ }
30
+
31
+ /**
32
+ * Interval ms of next execution
33
+ * @returns {number}
34
+ * @throws {Error} Always throws "Not Implemented Yet" error.
35
+ */
36
+ _next () {
37
+ const nextInterval = this._nextInterval + this.increasement
38
+ if (nextInterval >= this._max) {
39
+ this.increasement = -this.stepLength
40
+ return this._max
41
+ } else if (nextInterval <= this._min) {
42
+ this.increasement = this.stepLength
43
+ return this._min
44
+ }
45
+ return nextInterval
46
+ }
47
+ }
48
+
49
+ module.exports = ShuttlePolicy
package/lib/policy.js ADDED
@@ -0,0 +1,131 @@
1
+ 'use strict'
2
+ // internal
3
+ const {
4
+ TypeAssert: { assertPositive },
5
+ TypeUtils: { isNumber }
6
+ } = require('@creejs/commons-lang')
7
+ // owned
8
+ const { DefaultMaxInterval, DefaultMinInterval } = require('./constants')
9
+ // module vars
10
+
11
+ class Policy {
12
+ /**
13
+ * Creates a new Policy instance with specified retry bounds.
14
+ */
15
+ constructor () {
16
+ this._min = DefaultMinInterval
17
+ this._max = DefaultMaxInterval
18
+ this._nextInterval = this._min
19
+ }
20
+
21
+ /**
22
+ * Copies settings to target policy.
23
+ * @param {Policy} targetPolicy - The policy to modify.
24
+ */
25
+ copyPolicySetting (targetPolicy) {
26
+ targetPolicy.range(this._min, this._max)
27
+ targetPolicy._nextInterval = this._nextInterval
28
+ }
29
+
30
+ /**
31
+ * Sets a fixed interval retry policy.
32
+ }
33
+
34
+ /**
35
+ * Sets the minimum and maximum intervals for retries.
36
+ * @param {number} min - Minimum delay in milliseconds (must be positive and less than max)
37
+ * @param {number} max - Maximum delay in milliseconds (must be positive and greater than min)
38
+ * @returns {this} Returns the Retrier instance for chaining
39
+ * @throws {Error} If min is not less than max or if values are not positive
40
+ */
41
+ range (min, max) {
42
+ assertPositive(min, 'min')
43
+ assertPositive(max, 'max')
44
+ if (min >= max) {
45
+ throw new Error('min must < max')
46
+ }
47
+ this._min = min
48
+ if (this._nextInterval < this._min) {
49
+ this._nextInterval = this._min
50
+ }
51
+ this._max = max
52
+ if (this._nextInterval > this._max) {
53
+ this._nextInterval = this._max
54
+ }
55
+ return this
56
+ }
57
+
58
+ /**
59
+ * Sets the minimum retry delay in milliseconds.
60
+ * 1. will change currentInterval to min
61
+ * @param {number} min - The minimum delay (must be positive and less than max).
62
+ * @returns {this} The retrier instance for chaining.
63
+ * @throws {Error} If min is not positive or is greater than/equal to max.
64
+ */
65
+ min (min) {
66
+ assertPositive(min, 'min')
67
+ if (min >= this._max) {
68
+ throw new Error('min must < max')
69
+ }
70
+ this._min = min
71
+ this._nextInterval = this._min
72
+ return this
73
+ }
74
+
75
+ /**
76
+ * Sets the maximum retry retry delay in milliseconds.
77
+ * @param {number} max - The maximum delay (must be positive and greater than min).
78
+ * @throws {Error} If max is not greater than min.
79
+ * @returns {this} The retrier instance for chaining.
80
+ */
81
+ max (max) {
82
+ assertPositive(max, 'max')
83
+ if (max <= this._min) {
84
+ throw new Error('max must > min')
85
+ }
86
+ this._max = max
87
+ if (this._nextInterval > this._max) {
88
+ this._nextInterval = this._max
89
+ }
90
+ return this
91
+ }
92
+
93
+ reset () {
94
+ this._nextInterval = this._min
95
+ return this
96
+ }
97
+
98
+ /**
99
+ * Interval ms of next execution
100
+ * @returns {number}
101
+ * @throws {Error} Always throws "Not Implemented Yet" error.
102
+ */
103
+ generate () {
104
+ const rtnVal = this._nextInterval
105
+ this._increase()
106
+ return rtnVal
107
+ }
108
+
109
+ _increase () {
110
+ const nextInterval = this._next()
111
+ if (!isNumber(nextInterval)) {
112
+ throw new Error('Generated Next Interval Not Number')
113
+ }
114
+ if (nextInterval < this._min) {
115
+ return (this._nextInterval = this._min)
116
+ } else if (nextInterval > this._max) {
117
+ return (this._nextInterval = this._max)
118
+ }
119
+ return (this._nextInterval = nextInterval)
120
+ }
121
+
122
+ /**
123
+ * subclass should implement this method
124
+ * @returns {number} The interval in milliseconds to wait before the next retry attempt.
125
+ * @protected
126
+ */
127
+ _next () {
128
+ throw new Error('Not Impled Yet')
129
+ }
130
+ }
131
+ module.exports = Policy
@@ -0,0 +1,173 @@
1
+ 'use strict'
2
+ // owned
3
+ const Retrier = require('./retrier')
4
+ /**
5
+ * Creates a new Retrier instance with the specified name.
6
+ * @param {string} name - The name to assign to the retrier.
7
+ * @returns {Retrier} A new Retrier instance with the given name.
8
+ */
9
+ function name (name) {
10
+ const retrier = new Retrier()
11
+ retrier.name(name)
12
+ return retrier
13
+ }
14
+
15
+ /**
16
+ * Creates and returns a Retrier instance configured for infinite retries.
17
+ * @returns {Retrier} A Retrier instance with infinite retry behavior.
18
+ */
19
+ function infinite () {
20
+ const retrier = new Retrier()
21
+ retrier.infinite()
22
+ return retrier
23
+ }
24
+
25
+ /**
26
+ * Creates a retrier configured to attempt an operation a specified number of times.
27
+ * @param {number} times - The maximum number of retry attempts.
28
+ * @returns {Retrier} A configured Retrier instance with the specified max retries.
29
+ */
30
+ function times (times) {
31
+ const retrier = new Retrier()
32
+ retrier.times(times)
33
+ return retrier
34
+ }
35
+
36
+ /**
37
+ * Alias for times.
38
+ * @param {number} maxRetries - The maximum number of retry attempts.
39
+ * @returns {Retrier} A configured Retrier instance with the specified max retries.
40
+ */
41
+ function maxRetries (maxRetries) {
42
+ const retrier = new Retrier()
43
+ retrier.maxRetries(maxRetries)
44
+ return retrier
45
+ }
46
+
47
+ /**
48
+ * Sets the minimum Interval for the retrier and returns the instance.
49
+ * @param {number} min - The minimum Interval in milliseconds.
50
+ * @returns {Retrier} The retrier instance with updated minimum Interval.
51
+ */
52
+ function min (min) {
53
+ const retrier = new Retrier()
54
+ retrier.min(min)
55
+ return retrier
56
+ }
57
+
58
+ /**
59
+ * Sets the maximum Interval for the retrier and returns the instance.
60
+ * @param {number} max - The maximum Interval in milliseconds.
61
+ * @returns {Retrier} The retrier instance with updated maximum Interval.
62
+ */
63
+ function max (max) {
64
+ const retrier = new Retrier()
65
+ retrier.max(max)
66
+ return retrier
67
+ }
68
+
69
+ /**
70
+ * Creates a retrier with the specified Interval range.
71
+ * @param {number} min - Minimum Interval.
72
+ * @param {number} max - Maximum Interval.
73
+ * @returns {Retrier} A new Retrier instance configured with specified Interval range.
74
+ */
75
+ function range (min, max) {
76
+ const retrier = new Retrier()
77
+ retrier.range(min, max)
78
+ return retrier
79
+ }
80
+
81
+ /**
82
+ * Creates a retrier with a fixed interval between attempts.
83
+ * @param {number} fixedInterval - The fixed interval in milliseconds between retry attempts.
84
+ * @returns {Retrier} A new Retrier instance configured with the specified fixed interval.
85
+ */
86
+ function fixedInterval (fixedInterval) {
87
+ const retrier = new Retrier()
88
+ retrier.fixedInterval(fixedInterval)
89
+ return retrier
90
+ }
91
+
92
+ /**
93
+ * Creates a retrier with a fixed increase strategy.
94
+ * @param {number} increasement - The fixed amount to increase on each retry.
95
+ * @returns {Retrier} A retrier instance configured with fixed increase.
96
+ */
97
+ function fixedIncrease (increasement) {
98
+ const retrier = new Retrier()
99
+ retrier.fixedIncrease(increasement)
100
+ return retrier
101
+ }
102
+
103
+ /**
104
+ * Creates a new Retrier instance with factor-increase strategy.
105
+ * @param {number} factor - The factor by which to increase the interval on each retry.
106
+ * @returns {Retrier} A new Retrier instance with factor-increase strategy.
107
+ */
108
+ function factorIncrease (factor) {
109
+ const retrier = new Retrier()
110
+ retrier.factorIncrease(factor)
111
+ return retrier
112
+ }
113
+
114
+ /**
115
+ * Creates a Retrier instance with a shuttle-interval strategt.
116
+ * @param {number} stepLength - The interval step length of each change
117
+ * @returns {Retrier} A configured Retrier instance with shuttle-interval strategt.
118
+ */
119
+ function shuttleInterval (stepLength) {
120
+ const retrier = new Retrier()
121
+ retrier.shuttleInterval(stepLength)
122
+ return retrier
123
+ }
124
+
125
+ /**
126
+ * Creates a Retrier instance with a total-opertion timeout.
127
+ * @param {number} timeout - The timeout value in milliseconds.
128
+ * @returns {Retrier} A Retrier instance configured with the given timeout.
129
+ */
130
+ function timeout (timeout) {
131
+ const retrier = new Retrier()
132
+ retrier.timeout(timeout)
133
+ return retrier
134
+ }
135
+
136
+ /**
137
+ * Creates a retrier instance with a single-task timeout.
138
+ * @param {number} timeout - The timeout duration in milliseconds for the retrier task.
139
+ * @returns {Retrier} A Retrier instance configured with the given task timeout.
140
+ */
141
+ function taskTimeout (timeout) {
142
+ const retrier = new Retrier()
143
+ retrier.taskTimeout(timeout)
144
+ return retrier
145
+ }
146
+
147
+ /**
148
+ * Creates a new Retrier instance, sets the task to be retried, and starts the retry process.
149
+ * @param {Function} task - The asynchronous task function to be retried.
150
+ * @returns {Promise<*>} A promise that resolves when the retry process completes.
151
+ */
152
+ function start (task) {
153
+ const retrier = new Retrier()
154
+ retrier.task(task)
155
+ return retrier.start()
156
+ }
157
+
158
+ module.exports = {
159
+ name,
160
+ infinite,
161
+ times,
162
+ maxRetries,
163
+ min,
164
+ max,
165
+ range,
166
+ fixedInterval,
167
+ fixedIncrease,
168
+ factorIncrease,
169
+ shuttleInterval,
170
+ timeout,
171
+ taskTimeout,
172
+ start
173
+ }
package/lib/retrier.js ADDED
@@ -0,0 +1,534 @@
1
+ 'use strict'
2
+
3
+ // internal
4
+ const {
5
+ TypeAssert: { assertPositive, assertString, assertFunction, assertNumber },
6
+ TypeUtils: { isNil },
7
+ PromiseUtils
8
+ } = require('@creejs/commons-lang')
9
+ const { EventEmitter } = require('@creejs/commons-events')
10
+
11
+ // owned
12
+ // eslint-disable-next-line no-unused-vars
13
+ const Policy = require('./policy')
14
+ const Event = require('./event')
15
+ const FixedIntervalPolicy = require('./policy/fixed-interval-policy')
16
+ const FixedIncreasePolicy = require('./policy/fixed-increase-policy')
17
+ const FactoreIncreasePolicy = require('./policy/factor-increase-policy')
18
+ const ShuttlePolicy = require('./policy/shuttle-policy')
19
+ const Task = require('./task')
20
+ const AlwaysTask = require('./alway-task')
21
+ const { DefaultMaxRetries } = require('./constants')
22
+
23
+ // module vars
24
+ const TaskTimoutFlag = '!#@%$&^*'
25
+
26
+ /**
27
+ * @extends EventEmitter
28
+ */
29
+ class Retrier {
30
+ /**
31
+ * Creates a new Retrier instance with a fixed interval policy.
32
+ * @param {number} [fixedInterval=1000] - The fixed interval in milliseconds between retry attempts. Defaults to 1000ms if not provided.
33
+ */
34
+ constructor (fixedInterval) {
35
+ EventEmitter.mixin(this)
36
+ /**
37
+ * @type {Policy}
38
+ */
39
+ this._policy = new FixedIntervalPolicy(fixedInterval ?? 1000)
40
+ this._maxRetries = DefaultMaxRetries
41
+ this._currentRetries = 1
42
+ /**
43
+ * Timetou for total operation
44
+ * @type {number}
45
+ */
46
+ this._timeout = 120000 // 120s
47
+ /**
48
+ * Timetou for single task
49
+ */
50
+ this._taskTimeout = 2000 // 20s
51
+ this._name = 'unamed' // Retrier name
52
+
53
+ /**
54
+ * A Deferred Object as Singal to prevent Task concurrent start
55
+ * @type {{resolve:Function, reject:Function, promise: Promise<*>}|undefined}
56
+ */
57
+ this._taskingFlag = undefined
58
+
59
+ /**
60
+ * A Deferred Object as Singal to prevent Task concurrent stop
61
+ * @type {{resolve:Function, reject:Function, promise: Promise<*>}|undefined}
62
+ */
63
+ this._breakFlag = undefined
64
+ /**
65
+ * Reason for break
66
+ * @type {Error|undefined}
67
+ */
68
+ this._breakReason = undefined
69
+ }
70
+
71
+ get running () {
72
+ return !isNil(this._taskingFlag)
73
+ }
74
+
75
+ /**
76
+ * Sets the name of the retrier.
77
+ * @param {string} retrierName - The name to assign to the retrier.
78
+ * @returns {this} The retrier instance for chaining.
79
+ */
80
+ name (retrierName) {
81
+ assertString(retrierName, 'retrierName')
82
+ this._name = retrierName
83
+ return this
84
+ }
85
+
86
+ /**
87
+ * Sets the retry attempts to be infinite by setting max retries to maximum safe integer.
88
+ * @returns {Object} The retrier instance for chaining.
89
+ */
90
+ infinite () {
91
+ this._maxRetries = Infinity
92
+ return this
93
+ }
94
+
95
+ /**
96
+ * Sets the maximum number of retry attempts.
97
+ * @param {number} times - The maximum number of retries.
98
+ * @returns {this} The Retrier instance for chaining.
99
+ */
100
+ times (times) {
101
+ return this.maxRetries(times)
102
+ }
103
+
104
+ /**
105
+ * Sets the maximum number of retry attempts.
106
+ * @param {number} maxRetries - The maximum number of retries (must be positive).
107
+ * @returns {this} The retrier instance for chaining.
108
+ */
109
+ maxRetries (maxRetries) {
110
+ assertPositive(maxRetries, 'maxRetries')
111
+ this._maxRetries = maxRetries
112
+ return this
113
+ }
114
+
115
+ /**
116
+ * Sets the minimum retry delay in milliseconds.
117
+ * @param {number} min - The minimum delay (must be positive and less than max).
118
+ * @returns {this} The retrier instance for chaining.
119
+ * @throws {Error} If min is not positive or is greater than/equal to max.
120
+ */
121
+ min (min) {
122
+ this._policy.min(min)
123
+ return this
124
+ }
125
+
126
+ /**
127
+ * Sets the maximum retry retry delay in milliseconds.
128
+ * @param {number} max - The maximum delay (must be positive and greater than min).
129
+ * @throws {Error} If max is not greater than min.
130
+ * @returns {this} The retrier instance for chaining.
131
+ */
132
+ max (max) {
133
+ this._policy.max(max)
134
+ return this
135
+ }
136
+
137
+ /**
138
+ * Sets the minimum and maximum intervals for retries.
139
+ * @param {number} min - Minimum delay in milliseconds (must be positive and less than max)
140
+ * @param {number} max - Maximum delay in milliseconds (must be positive and greater than min)
141
+ * @returns {Retrier} Returns the Retrier instance for chaining
142
+ * @throws {Error} If min is not less than max or if values are not positive
143
+ */
144
+ range (min, max) {
145
+ this._policy.range(min, max)
146
+ return this
147
+ }
148
+
149
+ /**
150
+ * Sets a fixed interval retry policy.
151
+ * @param {number} fixedInterval - The fixed interval in milliseconds between retries.
152
+ * @returns {Retrier} The Retrier instance for chaining.
153
+ */
154
+ fixedInterval (fixedInterval) {
155
+ const oldPolicy = this._policy
156
+ if (oldPolicy instanceof FixedIntervalPolicy) {
157
+ oldPolicy.interval = fixedInterval
158
+ return this
159
+ }
160
+ const newPolicy = new FixedIntervalPolicy(fixedInterval)
161
+ oldPolicy?.copyPolicySetting(newPolicy)
162
+ newPolicy.reset()
163
+ this._policy = newPolicy
164
+ return this
165
+ }
166
+
167
+ /**
168
+ * Sets a fixed increase policy for retry intervals.
169
+ * @param {number} increasement - The fixed amount to increase the interval by on each retry.
170
+ * @returns {this} The retrier instance for chaining.
171
+ */
172
+ fixedIncrease (increasement) {
173
+ const oldPolicy = this._policy
174
+ if (oldPolicy instanceof FixedIncreasePolicy) {
175
+ oldPolicy.increasement = increasement
176
+ return this
177
+ }
178
+ const newPolicy = new FixedIncreasePolicy(increasement)
179
+ oldPolicy?.copyPolicySetting(newPolicy)
180
+ newPolicy.reset()
181
+ this._policy = newPolicy
182
+ return this
183
+ }
184
+
185
+ /**
186
+ * Sets a fixed increase factor for retry delays.
187
+ * @param {number} factor - The multiplier for delay increase between retries.
188
+ * @returns {this} The retrier instance for method chaining.
189
+ */
190
+ factorIncrease (factor) {
191
+ const oldPolicy = this._policy
192
+ if (oldPolicy instanceof FactoreIncreasePolicy) {
193
+ oldPolicy.factor = factor
194
+ return this
195
+ }
196
+ const newPolicy = new FactoreIncreasePolicy(factor)
197
+ oldPolicy?.copyPolicySetting(newPolicy)
198
+ newPolicy.reset()
199
+ this._policy = newPolicy
200
+ return this
201
+ }
202
+
203
+ /**
204
+ * Sets a shuttle retry policy with the given step length.
205
+ * @param {number} stepLength - The interval between retry attempts.
206
+ * @returns {this} The Retrier instance for chaining.
207
+ */
208
+ shuttleInterval (stepLength) {
209
+ const oldPolicy = this._policy
210
+ if (oldPolicy instanceof ShuttlePolicy) {
211
+ oldPolicy.stepLength = stepLength
212
+ return this
213
+ }
214
+ const newPolicy = new ShuttlePolicy(stepLength)
215
+ oldPolicy?.copyPolicySetting(newPolicy)
216
+ newPolicy.reset()
217
+ this._policy = newPolicy
218
+ return this
219
+ }
220
+
221
+ /**
222
+ * Sets the timeout duration for each Task execution.
223
+ * 1. must > 0
224
+ * @param {number} timeout - The timeout duration in milliseconds.
225
+ * @returns {Object} The retrier instance for chaining.
226
+ */
227
+ taskTimeout (timeout) {
228
+ assertPositive(timeout, 'timeout')
229
+ this._taskTimeout = timeout
230
+ return this
231
+ }
232
+
233
+ /**
234
+ * Sets the timeout duration for all retries.
235
+ * 1. <= 0 - no timeout
236
+ * 2. \> 0 - timeout duration in milliseconds
237
+ * @param {number} timeout - The timeout duration in milliseconds.
238
+ * @returns {Object} The retrier instance for chaining.
239
+ */
240
+ timeout (timeout) {
241
+ assertNumber(timeout, 'timeout')
242
+ this._timeout = timeout
243
+ return this
244
+ }
245
+
246
+ /**
247
+ * Sets the task function to be retried.
248
+ * @param {Function} task - The function to be executed and retried on failure.
249
+ * @returns {this} Returns the retrier instance for chaining.
250
+ */
251
+ task (task) {
252
+ assertFunction(task, 'task')
253
+ this._task = new Task(this, task)
254
+ return this
255
+ }
256
+
257
+ /**
258
+ * alias of {@linkcode Retrier.task()}
259
+ * @param {Function} task - The function to be executed and retried
260
+ * @return {this}
261
+ */
262
+ retry (task) {
263
+ this.task(task)
264
+ return this
265
+ }
266
+
267
+ /**
268
+ * Executes the given task, and never stop
269
+ * 1. if the task fails, will retry it after the interval generated by RetryPolicy
270
+ * 2. if the task succeeds, reset RetryPolicy to Minimum Interval and continue to run the task
271
+ * @param {Function} task - The async function to execute and retry.
272
+ * @param {boolean} [resetAfterSuccess=false] - Whether to reset retry counters after success.
273
+ * @returns {this} The Retrier instance for chaining.
274
+ */
275
+ always (task, resetAfterSuccess = false) {
276
+ this._task = new AlwaysTask(this, task, resetAfterSuccess)
277
+ return this
278
+ }
279
+
280
+ /**
281
+ * Starts the retry process.
282
+ * @returns {Promise<*>}
283
+ */
284
+ async start () {
285
+ if (this._task == null) {
286
+ throw new Error('No Task to Retry')
287
+ }
288
+ if (this._taskingFlag != null) {
289
+ return this._taskingFlag.promise
290
+ }
291
+ const startAt = Date.now()
292
+ let lastError = null
293
+ // @ts-ignore
294
+ this.emit(Event.Start, startAt)
295
+ this._taskingFlag = PromiseUtils.defer()
296
+ let latency = null
297
+ while (true) {
298
+ // need to stop?
299
+ if (this._breakFlag != null) {
300
+ this._taskingFlag.reject(this._breakReason)
301
+ break
302
+ }
303
+
304
+ latency = Date.now() - startAt
305
+
306
+ // total timeout?
307
+ if (!isInfinite(this._timeout) && latency >= this._timeout) { // total timeout
308
+ // @ts-ignore
309
+ this.emit(Event.Timeout, this._currentRetries, latency, this._timeout)
310
+ // always task, treat as success, resolve the whole promise with <void>
311
+ if (AlwaysTask.isAlwaysTask(this._task)) {
312
+ this._taskingFlag.resolve()
313
+ break
314
+ }
315
+ this._taskingFlag.reject(lastError ?? new Error(`Timeout "${this._timeout}" Exceeded`))
316
+ break
317
+ }
318
+
319
+ // @ts-ignore
320
+ this.emit(Event.Retry, this._currentRetries, latency)
321
+ const task = this._task // take task, it may be changed in events' callback functions
322
+ const nextDelay = this._policy.generate()
323
+ try {
324
+ try {
325
+ await PromiseUtils.timeout(task.execute(this._currentRetries, latency, nextDelay), this._taskTimeout, TaskTimoutFlag)
326
+ } catch (err) {
327
+ // @ts-ignore
328
+ if (err.message === TaskTimoutFlag) {
329
+ // @ts-ignore
330
+ this.emit(Event.TaskTimeout, this._currentRetries, latency, this._taskTimeout)
331
+ }
332
+ throw err
333
+ }
334
+ // @ts-ignore
335
+ if (task.failed) {
336
+ lastError = task.error
337
+ throw task.error
338
+ }
339
+ const rtnVal = task.result
340
+ // @ts-ignore
341
+ this.emit(Event.Success, rtnVal, this._currentRetries, latency)
342
+
343
+ // Not AwaysTask, we can finish all the retries with success
344
+ if (!AlwaysTask.isAlwaysTask(task)) {
345
+ this._taskingFlag.resolve(rtnVal)
346
+ break
347
+ }
348
+ // AwaysTask, continue to run the task
349
+ } catch (e) {
350
+ // @ts-ignore
351
+ this.emit(Event.Failure, e, this._currentRetries, latency)
352
+ }
353
+ const nextRetries = ++this._currentRetries
354
+ // next retry, max retries reached?
355
+ if (this._currentRetries > this._maxRetries) {
356
+ // @ts-ignore
357
+ this.emit(Event.MaxRetries, nextRetries, this._maxRetries)
358
+ // always task, treat as success, resolve the whole promise with <void>
359
+ if (AlwaysTask.isAlwaysTask(task)) {
360
+ this._taskingFlag.resolve()
361
+ break
362
+ }
363
+ this._taskingFlag.reject(lastError ?? new Error(`Max Retries Exceeded, Retring ${this._currentRetries} times > max ${this._maxRetries}`))
364
+ break
365
+ }
366
+ await PromiseUtils.delay(nextDelay)
367
+ }
368
+ this._taskingFlag.promise.finally(() => {
369
+ this.resetRetryPolicy()
370
+ this._taskingFlag = undefined
371
+ const spent = Date.now() - startAt
372
+ // @ts-ignore
373
+ this.emit(Event.Completed, this._currentRetries, spent)
374
+ })
375
+ return this._taskingFlag.promise
376
+ }
377
+
378
+ /**
379
+ * Stops the retrier with an optional reason. If already stopping, returns the existing break promise.
380
+ * @param {Error} [reason] - Optional reason for stopping (defaults to 'Manually Stop' error).
381
+ * @returns {Promise<void>} A promise that resolves when the retrier has fully stopped.
382
+ */
383
+ async stop (reason) {
384
+ // @ts-ignore
385
+ this.emit(Event.Stop, reason)
386
+ if (this._taskingFlag == null) {
387
+ return // no task running
388
+ }
389
+ if (this._breakFlag != null) {
390
+ // @ts-ignore
391
+ return this._breakFlag.promise
392
+ }
393
+ this._breakFlag = PromiseUtils.defer()
394
+ this._breakReason = reason ?? new Error('Manually Stop')
395
+ // @ts-ignore
396
+ this.once(Event.Completed, () => {
397
+ // @ts-ignore
398
+ this._breakFlag.resolve()
399
+ })
400
+
401
+ // @ts-ignore
402
+ this._breakFlag.promise.finally(() => {
403
+ this._breakFlag = undefined
404
+ this._breakReason = undefined
405
+ })
406
+ return this._breakFlag.promise
407
+ }
408
+
409
+ /**
410
+ * Resets the retry policy to its initial state.
411
+ */
412
+ resetRetryPolicy () {
413
+ this._policy.reset()
414
+ }
415
+
416
+ /**
417
+ * Registers a listener function to be called on "retry" events.
418
+ * @param {Function} listener - The callback function
419
+ * @returns {this}
420
+ */
421
+ onRetry (listener) {
422
+ // @ts-ignore
423
+ this.on(Event.Retry, listener)
424
+ return this
425
+ }
426
+
427
+ /**
428
+ * Registers a listener for "error" events.
429
+ * @param {Function} listener - The callback function
430
+ * @returns {this}
431
+ */
432
+ onError (listener) {
433
+ // @ts-ignore
434
+ this.on(Event.Error, listener)
435
+ return this
436
+ }
437
+
438
+ /**
439
+ * Registers a listener for "failure" events.
440
+ * @param {Function} listener - The callback function
441
+ * @returns {this}
442
+ */
443
+ onFailure (listener) {
444
+ // @ts-ignore
445
+ this.on(Event.Failure, listener)
446
+ return this
447
+ }
448
+
449
+ /**
450
+ * Registers a listener for "success" events.
451
+ * @param {Function} listener - The callback function
452
+ * @returns {this}
453
+ */
454
+ onSuccess (listener) {
455
+ // @ts-ignore
456
+ this.on(Event.Success, listener)
457
+ return this
458
+ }
459
+
460
+ /**
461
+ * Registers a listener for "start" events.
462
+ * @param {Function} listener - The callback function
463
+ * @returns {this}
464
+ */
465
+ onStart (listener) {
466
+ // @ts-ignore
467
+ this.on(Event.Start, listener)
468
+ return this
469
+ }
470
+
471
+ /**
472
+ * Registers a listener for "stop" events.
473
+ * @param {Function} listener - The callback function
474
+ * @returns {this}
475
+ */
476
+ onStop (listener) {
477
+ // @ts-ignore
478
+ this.on(Event.Stop, listener)
479
+ return this
480
+ }
481
+
482
+ /**
483
+ * Registers a listener for "timeout" events.
484
+ * @param {Function} listener - The callback function
485
+ */
486
+ onTimeout (listener) {
487
+ // @ts-ignore
488
+ this.on(Event.Timeout, listener)
489
+ return this
490
+ }
491
+
492
+ /**
493
+ * Registers a listener for "task-timeout" events.
494
+ * @param {Function} listener - The callback function
495
+ * @returns {this}
496
+ */
497
+ onTaskTimeout (listener) {
498
+ // @ts-ignore
499
+ this.on(Event.TaskTimeout, listener)
500
+ return this
501
+ }
502
+
503
+ /**
504
+ * Registers a listener for "completed" events.
505
+ * @param {Function} listener - The callback function
506
+ */
507
+ onCompleted (listener) {
508
+ // @ts-ignore
509
+ this.on(Event.Completed, listener)
510
+ return this
511
+ }
512
+
513
+ /**
514
+ * Registers a listener for the 'MaxRetries' event.
515
+ * @param {Function} listener - The callback function to be executed when max retries are reached.
516
+ * @returns {this}
517
+ */
518
+ onMaxRetries (listener) {
519
+ // @ts-ignore
520
+ this.on(Event.MaxRetries, listener)
521
+ return this
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Checks if a value represents infinity or a non-positive number.
527
+ * @param {number} value - The value to check
528
+ * @returns {boolean} True if the value is <= 0 or Infinity, false otherwise
529
+ */
530
+ function isInfinite (value) {
531
+ return value <= 0 || value === Infinity
532
+ }
533
+
534
+ module.exports = Retrier
package/lib/task.js ADDED
@@ -0,0 +1,56 @@
1
+ 'use strict'
2
+
3
+ const { TypeAssert: { assertNotNil, assertFunction } } = require('@creejs/commons-lang')
4
+ // owned
5
+ // eslint-disable-next-line no-unused-vars
6
+ const Retrier = require('./retrier')
7
+
8
+ class Task {
9
+ /**
10
+ * Creates a new Task instance.
11
+ * @param {Retrier} retrier - The retrier instance.
12
+ * @param {Function} task - The function to be executed as the task.
13
+ */
14
+ constructor (retrier, task) {
15
+ assertNotNil(retrier, 'retrier')
16
+ assertFunction(task, 'task')
17
+ this.retrier = retrier
18
+ this.task = task
19
+ this.result = undefined
20
+ this.error = undefined
21
+ }
22
+
23
+ get failed () {
24
+ return this.error != null
25
+ }
26
+
27
+ get succeeded () {
28
+ return this.error == null
29
+ }
30
+
31
+ /**
32
+ * Executes the task with the given retry parameters.
33
+ * 1. if execution throw error, keep error in this.error
34
+ * 2. if execution return value, keep it in this.result
35
+ * 3. always return Promise<void>
36
+ * @param {number} retries - The number of retries attempted so far.
37
+ * @param {number} latence - The current latency ms.
38
+ * @param {number} nextInterval - The next interval ms.
39
+ * @returns {Promise<void>} The result of the task execution.
40
+ */
41
+ async execute (retries, latence, nextInterval) {
42
+ try {
43
+ this.result = await this.task(retries, latence, nextInterval)
44
+ this.error = undefined
45
+ } catch (e) {
46
+ this.error = e
47
+ }
48
+ }
49
+
50
+ dispose () {
51
+ // @ts-ignore
52
+ this.retrier = undefined
53
+ }
54
+ }
55
+
56
+ module.exports = Task
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@creejs/commons-retrier",
3
+ "version": "1.0.0",
4
+ "description": "Common Utils About Task Retrying",
5
+ "main": "index.js",
6
+ "private": false,
7
+ "files": [
8
+ "index.js",
9
+ "lib/",
10
+ "types/",
11
+ "README.md"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/frameworkee/commons.git"
19
+ },
20
+ "scripts": {
21
+ "dts": "tsc",
22
+ "generate-docs": "node_modules/.bin/jsdoc -c ./jsdoc.json"
23
+ },
24
+ "author": "rodney.vin@gmail.com",
25
+ "license": "Apache-2.0",
26
+ "dependencies": {
27
+ "@creejs/commons-lang": "^1.0.0",
28
+ "@creejs/commons-events": "^1.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "better-docs": "^2.7.3",
32
+ "jsdoc": "^4.0.4"
33
+ }
34
+ }