182 lines
4.7 KiB
JavaScript
182 lines
4.7 KiB
JavaScript
function e(e) {
|
|
'@hwc/retry' === globalThis?.process?.env.DEBUG && console.debug(e);
|
|
}
|
|
class RetryTask {
|
|
id = Math.random().toString(36).slice(2);
|
|
fn;
|
|
error;
|
|
timestamp = Date.now();
|
|
lastAttempt = this.timestamp;
|
|
resolve;
|
|
reject;
|
|
signal;
|
|
constructor(e, t, r, i, s) {
|
|
(this.fn = e),
|
|
(this.error = t),
|
|
(this.timestamp = Date.now()),
|
|
(this.lastAttempt = Date.now()),
|
|
(this.resolve = r),
|
|
(this.reject = i),
|
|
(this.signal = s);
|
|
}
|
|
get age() {
|
|
return Date.now() - this.timestamp;
|
|
}
|
|
}
|
|
class Retrier {
|
|
#e = [];
|
|
#t = [];
|
|
#r = 0;
|
|
#i;
|
|
#s;
|
|
#n;
|
|
#o;
|
|
#c;
|
|
constructor(
|
|
e,
|
|
{ timeout: t = 6e4, maxDelay: r = 100, concurrency: i = 1e3 } = {}
|
|
) {
|
|
if ('function' != typeof e)
|
|
throw new Error('Missing function to check errors');
|
|
(this.#o = e), (this.#i = t), (this.#s = r), (this.#c = i);
|
|
}
|
|
get retrying() {
|
|
return this.#e.length;
|
|
}
|
|
get pending() {
|
|
return this.#t.length;
|
|
}
|
|
get working() {
|
|
return this.#r;
|
|
}
|
|
#a(t, { signal: r, promise: i, resolve: s, reject: n }) {
|
|
let o;
|
|
try {
|
|
o = t();
|
|
} catch (e) {
|
|
return n(new Error(`Synchronous error: ${e.message}`, { cause: e })), i;
|
|
}
|
|
return o && 'function' == typeof o.then ?
|
|
(this.#r++,
|
|
i
|
|
.finally(() => {
|
|
this.#r--, this.#h();
|
|
})
|
|
.catch(() => {}),
|
|
Promise.resolve(o)
|
|
.then((t) => {
|
|
e('Function called successfully without retry.'), s(t);
|
|
})
|
|
.catch((i) => {
|
|
if (!this.#o(i)) return void n(i);
|
|
const o = new RetryTask(t, i, s, n, r);
|
|
e(`Function failed, queuing for retry with task ${o.id}.`),
|
|
this.#e.push(o),
|
|
r?.addEventListener('abort', () => {
|
|
e(`Task ${o.id} was aborted due to AbortSignal.`), n(r.reason);
|
|
}),
|
|
this.#g();
|
|
}),
|
|
i)
|
|
: (n(new Error('Result is not a promise.')), i);
|
|
}
|
|
retry(e, { signal: t } = {}) {
|
|
t?.throwIfAborted();
|
|
const {
|
|
promise: r,
|
|
resolve: i,
|
|
reject: s,
|
|
} = (function () {
|
|
if (Promise.withResolvers) return Promise.withResolvers();
|
|
let e, t;
|
|
const r = new Promise((r, i) => {
|
|
(e = r), (t = i);
|
|
});
|
|
if (void 0 === e || void 0 === t)
|
|
throw new Error(
|
|
'Promise executor did not initialize resolve or reject.'
|
|
);
|
|
return { promise: r, resolve: e, reject: t };
|
|
})();
|
|
return (
|
|
this.#t.push(() =>
|
|
this.#a(e, { signal: t, promise: r, resolve: i, reject: s })
|
|
),
|
|
this.#h(),
|
|
r
|
|
);
|
|
}
|
|
#u() {
|
|
this.pending && this.#h(), this.retrying && this.#g();
|
|
}
|
|
#h() {
|
|
e(
|
|
`Processing pending tasks: ${this.pending} pending, ${this.working} working.`
|
|
);
|
|
const t = this.#c - this.working;
|
|
if (t <= 0) return;
|
|
const r = Math.min(this.pending, t);
|
|
for (let e = 0; e < r; e++) {
|
|
const e = this.#t.shift();
|
|
e?.();
|
|
}
|
|
e(
|
|
`Processed pending tasks: ${this.pending} pending, ${this.working} working.`
|
|
);
|
|
}
|
|
#g() {
|
|
clearTimeout(this.#n),
|
|
(this.#n = void 0),
|
|
e(
|
|
`Processing retry queue: ${this.retrying} retrying, ${this.working} working.`
|
|
);
|
|
const t = () => {
|
|
this.#n = setTimeout(() => this.#u(), 0);
|
|
},
|
|
r = this.#e.shift();
|
|
return (
|
|
r ?
|
|
(
|
|
(function (e, t) {
|
|
return e.age > t;
|
|
})(r, this.#i)
|
|
) ?
|
|
(e(`Task ${r.id} was abandoned due to timeout.`),
|
|
r.reject(r.error),
|
|
void t())
|
|
: (
|
|
(function (e, t) {
|
|
const r = Date.now() - e.lastAttempt,
|
|
i = Math.max(e.lastAttempt - e.timestamp, 1);
|
|
return r >= Math.min(1.2 * i, t);
|
|
})(r, this.#s)
|
|
) ?
|
|
((r.lastAttempt = Date.now()),
|
|
void Promise.resolve(r.fn())
|
|
.then((t) => {
|
|
e(`Task ${r.id} succeeded after ${r.age}ms.`), r.resolve(t);
|
|
})
|
|
.catch((t) => {
|
|
if (!this.#o(t))
|
|
return (
|
|
e(
|
|
`Task ${r.id} failed with non-retryable error: ${t.message}.`
|
|
),
|
|
void r.reject(t)
|
|
);
|
|
(r.lastAttempt = Date.now()),
|
|
this.#e.push(r),
|
|
e(`Task ${r.id} failed, requeueing to try again.`);
|
|
})
|
|
.finally(() => {
|
|
this.#u();
|
|
}))
|
|
: (e(`Task ${r.id} is not ready to retry, skipping.`),
|
|
this.#e.push(r),
|
|
void t())
|
|
: (e('Queue is empty, exiting.'), void (this.pending && t()))
|
|
);
|
|
}
|
|
}
|
|
export { Retrier };
|