2025-04-19 23:12:19 -04:00

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 };