Initial commit: ERP system with advance verification fixes
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2011 Tim Koschützki (tim@debuggable.com), Felix Geisendörfer (felix@debuggable.com)
|
||||
Copyright (c) 2014 IndigoUnited
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
# @gar/promise-retry
|
||||
|
||||
This is a fork of [promise-retry](https://npm.im/promise-retry). See the [CHANGELOG.md](./CHANGELOG.md) for more info.
|
||||
It also inlines and updates the original [retry](https://github.com/tim-kos/node-retry) package that was being promisified.
|
||||
|
||||
Retries a function that returns a promise, leveraging the power of the [retry](https://github.com/tim-kos/node-retry) module to the promises world.
|
||||
|
||||
There's already some modules that are able to retry functions that return promises but they were rather difficult to use or do not offer an easy way to do conditional retries.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
`$ npm install promise-retry`
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### retry(fn(retry, number, operation), [options])
|
||||
|
||||
Calls `fn` until the returned promise ends up fulfilled or rejected with an error different than a `retry` error.
|
||||
The `options` argument is an object which maps to the original [retry](https://github.com/tim-kos/node-retry) module options:
|
||||
|
||||
- `retries`: The maximum amount of times to retry the operation. Default is `10`.
|
||||
- `factor`: The exponential factor to use. Default is `2`.
|
||||
- `minTimeout`: The number of milliseconds before starting the first retry. Default is `1000`.
|
||||
- `maxTimeout`: The maximum number of milliseconds between two retries. Default is `Infinity`.
|
||||
- `randomize`: Randomizes the timeouts by multiplying with a factor between `1` to `2`. Default is `false`.
|
||||
- `forever`: Whether to retry forver, default is false.
|
||||
- `unref`: Whether to [unref](https://nodejs.org/api/timers.html#timers_unref) the underlying `setTimeout`s.
|
||||
- `maxRetryTime`: Maximum number of milliseconds that the retried operation is allowed to run.
|
||||
|
||||
`options` can also be a Number, which is effectively the same as pasing `{ retries: Number }`
|
||||
|
||||
|
||||
The `fn` function will be called with the following parameters:
|
||||
- A `retry` function as its first argument that should be called with an error whenever you want to retry `fn`. The `retry` function will always throw an error.
|
||||
- The current retry number being attempted
|
||||
- The retry operation object itself from which will allow you to call things like `operation.reset()`
|
||||
|
||||
If there are retries left, it will throw a special `retry` error that will be handled internally to call `fn` again.
|
||||
If there are no retries left, it will throw the actual error passed to it.
|
||||
|
||||
## Example
|
||||
```js
|
||||
const { retry } = require('@gar/promise-retry');
|
||||
|
||||
// Simple example
|
||||
retry(function (retry, number) {
|
||||
console.log('attempt number', number);
|
||||
|
||||
return doSomething()
|
||||
.catch(retry);
|
||||
})
|
||||
.then(function (value) {
|
||||
// ..
|
||||
}, function (err) {
|
||||
// ..
|
||||
});
|
||||
|
||||
// Conditional example
|
||||
retry(function (retry, number) {
|
||||
console.log('attempt number', number);
|
||||
|
||||
return doSomething()
|
||||
.catch(function (err) {
|
||||
if (err.code === 'ETIMEDOUT') {
|
||||
retry(err);
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
})
|
||||
.then(function (value) {
|
||||
// ..
|
||||
}, function (err) {
|
||||
// ..
|
||||
});
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
`$ npm test`
|
||||
|
||||
## License
|
||||
|
||||
Released under the [MIT License](http://www.opensource.org/licenses/mit-license.php).
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
type OperationOptions = {
|
||||
/**
|
||||
* The exponential factor to use.
|
||||
* @default 2
|
||||
*/
|
||||
factor?: number | undefined;
|
||||
/**
|
||||
* The number of milliseconds before starting the first retry.
|
||||
* @default 1000
|
||||
*/
|
||||
minTimeout?: number | undefined;
|
||||
/**
|
||||
* The maximum number of milliseconds between two retries.
|
||||
* @default Infinity
|
||||
*/
|
||||
maxTimeout?: number | undefined;
|
||||
/**
|
||||
* Randomizes the timeouts by multiplying a factor between 1-2.
|
||||
* @default false
|
||||
*/
|
||||
randomize?: boolean | undefined;
|
||||
/**
|
||||
* The maximum amount of times to retry the operation.
|
||||
* @default 10
|
||||
*/
|
||||
retries?: number | undefined;
|
||||
/**
|
||||
* Whether to retry forever.
|
||||
* @default false
|
||||
*/
|
||||
forever?: boolean | undefined;
|
||||
/**
|
||||
* Whether to [unref](https://nodejs.org/api/timers.html#timers_unref) the setTimeout's.
|
||||
* @default false
|
||||
*/
|
||||
unref?: boolean | undefined;
|
||||
/**
|
||||
* The maximum time (in milliseconds) that the retried operation is allowed to run.
|
||||
* @default Infinity
|
||||
*/
|
||||
maxRetryTime?: number | undefined;
|
||||
} | number[];
|
||||
|
||||
type RetryOperation = {
|
||||
/**
|
||||
* Returns an array of all errors that have been passed to `retryOperation.retry()` so far.
|
||||
* The returning array has the errors ordered chronologically based on when they were passed to
|
||||
* `retryOperation.retry()`, which means the first passed error is at index zero and the last is at the last index.
|
||||
*/
|
||||
errors(): Error[];
|
||||
|
||||
/**
|
||||
* A reference to the error object that occured most frequently.
|
||||
* Errors are compared using the `error.message` property.
|
||||
* If multiple error messages occured the same amount of time, the last error object with that message is returned.
|
||||
*
|
||||
* @return If no errors occured so far the value will be `null`.
|
||||
*/
|
||||
mainError(): Error | null;
|
||||
|
||||
/**
|
||||
* Defines the function that is to be retried and executes it for the first time right away.
|
||||
*
|
||||
* @param fn The function that is to be retried. `currentAttempt` represents the number of attempts callback has been executed so far.
|
||||
*/
|
||||
attempt(fn: (currentAttempt: number) => void): void;
|
||||
|
||||
/**
|
||||
* Returns `false` when no `error` value is given, or the maximum amount of retries has been reached.
|
||||
* Otherwise it returns `true`, and retries the operation after the timeout for the current attempt number.
|
||||
*/
|
||||
retry(err?: Error): boolean;
|
||||
|
||||
/**
|
||||
* Stops the operation being retried. Useful for aborting the operation on a fatal error etc.
|
||||
*/
|
||||
stop(): void;
|
||||
|
||||
/**
|
||||
* Resets the internal state of the operation object, so that you can call `attempt()` again as if
|
||||
* this was a new operation object.
|
||||
*/
|
||||
reset(): void;
|
||||
|
||||
/**
|
||||
* Returns an int representing the number of attempts it took to call `fn` before it was successful.
|
||||
*/
|
||||
attempts(): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that is retryable, by having implicitly-bound params for both an error handler and an attempt number.
|
||||
*
|
||||
* @param retry The retry callback upon any rejection. Essentially throws the error on in the form of a { retried: err }
|
||||
* wrapper, and tags it with a 'code' field of value "EPROMISERETRY" so that it is recognised as needing retrying. Call
|
||||
* this from the catch() block when you want to retry a rejected attempt.
|
||||
* @param attempt The number of the attempt.
|
||||
* @param operation The operation object from the underlying retry module.
|
||||
* @returns A Promise for anything (eg. a HTTP response).
|
||||
*/
|
||||
type RetryableFn<ResolutionType> = (retry: (error: any) => never, attempt: number, operation: RetryOperation) => Promise<ResolutionType>;
|
||||
/**
|
||||
* Wrap all functions of the object with retry. The params can be entered in either order, just like in the original library.
|
||||
*
|
||||
* @param retryableFn The function to retry.
|
||||
* @param options The options for how long/often to retry the function for.
|
||||
* @returns The Promise resolved by the input retryableFn, or rejected (if not retried) from its catch block.
|
||||
*/
|
||||
declare function promiseRetry<ResolutionType>(
|
||||
retryableFn: RetryableFn<ResolutionType>,
|
||||
options?: OperationOptions,
|
||||
): Promise<ResolutionType>;
|
||||
|
||||
export { promiseRetry };
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
const { RetryOperation } = require('./retry')
|
||||
|
||||
const createTimeout = (attempt, opts) => Math.min(Math.round((1 + (opts.randomize ? Math.random() : 0)) * Math.max(opts.minTimeout, 1) * Math.pow(opts.factor, attempt)), opts.maxTimeout)
|
||||
const isRetryError = err => err?.code === 'EPROMISERETRY' && Object.hasOwn(err, 'retried')
|
||||
|
||||
const promiseRetry = async (fn, options = {}) => {
|
||||
let timeouts = []
|
||||
if (options instanceof Array) {
|
||||
timeouts = [...options]
|
||||
} else {
|
||||
if (options.retries === Infinity) {
|
||||
options.forever = true
|
||||
delete options.retries
|
||||
}
|
||||
const opts = {
|
||||
retries: 10,
|
||||
factor: 2,
|
||||
minTimeout: 1 * 1000,
|
||||
maxTimeout: Infinity,
|
||||
randomize: false,
|
||||
...options
|
||||
}
|
||||
if (opts.minTimeout > opts.maxTimeout) {
|
||||
throw new Error('minTimeout is greater than maxTimeout')
|
||||
}
|
||||
if (opts.retries) {
|
||||
for (let i = 0; i < opts.retries; i++) {
|
||||
timeouts.push(createTimeout(i, opts))
|
||||
}
|
||||
// sort the array numerically ascending (since the timeouts may be out of order at factor < 1)
|
||||
timeouts.sort((a, b) => a - b)
|
||||
} else if (options.forever) {
|
||||
timeouts.push(createTimeout(0, opts))
|
||||
}
|
||||
}
|
||||
|
||||
const operation = new RetryOperation(timeouts, {
|
||||
forever: options.forever,
|
||||
unref: options.unref,
|
||||
maxRetryTime: options.maxRetryTime
|
||||
})
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
operation.attempt(async number => {
|
||||
try {
|
||||
const result = await fn(err => {
|
||||
throw Object.assign(new Error('Retrying'), { code: 'EPROMISERETRY', retried: err })
|
||||
}, number, operation)
|
||||
return resolve(result)
|
||||
} catch (err) {
|
||||
if (!isRetryError(err)) {
|
||||
return reject(err)
|
||||
}
|
||||
if (!operation.retry(err.retried || new Error())) {
|
||||
return reject(err.retried)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { promiseRetry }
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
class RetryOperation {
|
||||
#attempts = 1
|
||||
#cachedTimeouts = null
|
||||
#errors = []
|
||||
#fn = null
|
||||
#maxRetryTime
|
||||
#operationStart = null
|
||||
#originalTimeouts
|
||||
#timeouts
|
||||
#timer = null
|
||||
#unref
|
||||
|
||||
constructor (timeouts, options = {}) {
|
||||
this.#originalTimeouts = [...timeouts]
|
||||
this.#timeouts = [...timeouts]
|
||||
this.#unref = options.unref
|
||||
this.#maxRetryTime = options.maxRetryTime || Infinity
|
||||
if (options.forever) {
|
||||
this.#cachedTimeouts = [...this.#timeouts]
|
||||
}
|
||||
}
|
||||
|
||||
get timeouts () {
|
||||
return [...this.#timeouts]
|
||||
}
|
||||
|
||||
get errors () {
|
||||
return [...this.#errors]
|
||||
}
|
||||
|
||||
get attempts () {
|
||||
return this.#attempts
|
||||
}
|
||||
|
||||
get mainError () {
|
||||
let mainError = null
|
||||
if (this.#errors.length) {
|
||||
let mainErrorCount = 0
|
||||
const counts = {}
|
||||
for (let i = 0; i < this.#errors.length; i++) {
|
||||
const error = this.#errors[i]
|
||||
const { message } = error
|
||||
if (!counts[message]) {
|
||||
counts[message] = 0
|
||||
}
|
||||
counts[message]++
|
||||
|
||||
if (counts[message] >= mainErrorCount) {
|
||||
mainError = error
|
||||
mainErrorCount = counts[message]
|
||||
}
|
||||
}
|
||||
}
|
||||
return mainError
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.#attempts = 1
|
||||
this.#timeouts = [...this.#originalTimeouts]
|
||||
}
|
||||
|
||||
stop () {
|
||||
if (this.#timer) {
|
||||
clearTimeout(this.#timer)
|
||||
}
|
||||
|
||||
this.#timeouts = []
|
||||
this.#cachedTimeouts = null
|
||||
}
|
||||
|
||||
retry (err) {
|
||||
this.#errors.push(err)
|
||||
if (new Date().getTime() - this.#operationStart >= this.#maxRetryTime) {
|
||||
// XXX This puts the timeout error first, meaning it will never show as mainError, there may be no way to ever see this
|
||||
this.#errors.unshift(new Error('RetryOperation timeout occurred'))
|
||||
return false
|
||||
}
|
||||
|
||||
let timeout = this.#timeouts.shift()
|
||||
if (timeout === undefined) {
|
||||
// We're out of timeouts, clear the last error and repeat the final timeout
|
||||
if (this.#cachedTimeouts) {
|
||||
this.#errors.pop()
|
||||
timeout = this.#cachedTimeouts.at(-1)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// TODO what if there already is a timer?
|
||||
this.#timer = setTimeout(() => {
|
||||
this.#attempts++
|
||||
this.#fn(this.#attempts)
|
||||
}, timeout)
|
||||
|
||||
if (this.#unref) {
|
||||
this.#timer.unref()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
attempt (fn) {
|
||||
this.#fn = fn
|
||||
this.#operationStart = new Date().getTime()
|
||||
this.#fn(this.#attempts)
|
||||
}
|
||||
}
|
||||
module.exports = { RetryOperation }
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@gar/promise-retry",
|
||||
"version": "1.0.3",
|
||||
"description": "Retries a function that returns a promise, leveraging the power of the retry module.",
|
||||
"main": "./lib/index.js",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"type": "commonjs",
|
||||
"exports": {
|
||||
".": [
|
||||
{
|
||||
"default": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts"
|
||||
},
|
||||
"./lib/index.js"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "npx standard",
|
||||
"lint:fix": "npx standard --fix",
|
||||
"test": "node --test --experimental-test-coverage --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=100",
|
||||
"typelint": "npx -p typescript tsc ./lib/index.d.ts",
|
||||
"posttest": "npm run lint",
|
||||
"postlint": "npm run typelint"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/wraithgar/node-promise-retry/issues/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/wraithgar/node-promise-retry.git"
|
||||
},
|
||||
"keywords": [
|
||||
"retry",
|
||||
"promise",
|
||||
"backoff",
|
||||
"repeat",
|
||||
"replay"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user