Initial commit: ERP system with advance verification fixes

This commit is contained in:
System Administrator
2026-03-25 23:55:36 +07:00
commit 563ca12d76
5920 changed files with 828689 additions and 0 deletions
@@ -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 };
@@ -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 }
@@ -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 }