Update src/libpoketube/libpoketube-core.js
This commit is contained in:
parent
d5c5e5c8cd
commit
b6684cf08a
@ -59,66 +59,25 @@ class InnerTubePokeVidious {
|
|||||||
"User-Agent": this.useragent,
|
"User-Agent": this.useragent,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// retry until success, but enforce a 5-second max retry window only if it fails
|
||||||
// retry indefinitely but with a 5-second max retry window to avoid spam
|
|
||||||
const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
||||||
const startTime = Date.now();
|
|
||||||
let lastError;
|
let lastError;
|
||||||
|
|
||||||
// Retryable HTTP statuses
|
// retryable HTTP statuses
|
||||||
const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
|
const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
|
||||||
|
|
||||||
// Backoff settings
|
// backoff settings
|
||||||
const BASE_DELAY_MS = 120; // initial backoff base
|
const BASE_DELAY_MS = 120;
|
||||||
const MAX_DELAY_MS = 1000; // cap between attempts
|
const MAX_DELAY_MS = 1000;
|
||||||
const MIN_DELAY_MS = 50; // never spin
|
const MIN_DELAY_MS = 50;
|
||||||
const JITTER_FRAC = 0.2; // +/- 20% jitter
|
const JITTER_FRAC = 0.2;
|
||||||
|
|
||||||
// Per-attempt timeout (capped by remaining retry window)
|
|
||||||
const DEFAULT_PER_TRY_TIMEOUT = 2000; // soft cap per attempt
|
|
||||||
|
|
||||||
// Respect caller's AbortSignal if provided
|
|
||||||
const callerSignal = options.signal;
|
|
||||||
|
|
||||||
// Merge caller signal with a per-attempt timeout signal
|
|
||||||
const withTimeoutSignal = (ms) => {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timer = setTimeout(() => controller.abort(new Error("Fetch attempt timed out")), ms);
|
|
||||||
|
|
||||||
// If caller aborts, propagate to controller
|
|
||||||
const onCallerAbort = () => controller.abort(callerSignal.reason || new Error("Aborted by caller"));
|
|
||||||
if (callerSignal) {
|
|
||||||
if (callerSignal.aborted) {
|
|
||||||
controller.abort(callerSignal.reason || new Error("Aborted by caller"));
|
|
||||||
} else {
|
|
||||||
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup hook for the attempt
|
|
||||||
const cleanup = () => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
if (callerSignal) callerSignal.removeEventListener("abort", onCallerAbort);
|
|
||||||
};
|
|
||||||
|
|
||||||
return { signal: controller.signal, cleanup };
|
|
||||||
};
|
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
|
let retryStart = null;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const remaining = maxRetryTime - elapsed;
|
|
||||||
if (remaining <= 0) {
|
|
||||||
throw lastError || new Error(`Fetch failed for ${url} after ${maxRetryTime}ms`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-attempt timeout is the lesser of DEFAULT and remaining (leave a small buffer)
|
|
||||||
const perTryTimeout = Math.max(1, Math.min(DEFAULT_PER_TRY_TIMEOUT, remaining - 10));
|
|
||||||
const { signal, cleanup } = withTimeoutSignal(perTryTimeout);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
@ -126,67 +85,49 @@ const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
|||||||
...options.headers,
|
...options.headers,
|
||||||
...headers,
|
...headers,
|
||||||
},
|
},
|
||||||
signal,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
cleanup();
|
return res; // success: return immediately
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only start timing if we need to retry
|
||||||
|
if (!retryStart) retryStart = Date.now();
|
||||||
|
|
||||||
if (!RETRYABLE_STATUS.has(res.status)) {
|
if (!RETRYABLE_STATUS.has(res.status)) {
|
||||||
cleanup();
|
return res; // non-retryable status, return immediately
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this?.initError?.(`Retrying fetch for ${url}`, res.status);
|
this?.initError?.(`Retrying fetch for ${url}`, res.status);
|
||||||
|
|
||||||
// Decide next delay with exponential backoff + jitter, but keep within remaining window
|
if (Date.now() - retryStart >= maxRetryTime) {
|
||||||
|
throw new Error(`Fetch failed for ${url} after ${maxRetryTime}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
const rawDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
|
const rawDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
|
||||||
const jitter = rawDelay * JITTER_FRAC;
|
const jitter = rawDelay * JITTER_FRAC;
|
||||||
let delay = rawDelay + (Math.random() * 2 * jitter - jitter);
|
let delay = rawDelay + (Math.random() * 2 * jitter - jitter);
|
||||||
delay = Math.max(MIN_DELAY_MS, Math.min(delay, remaining - 1));
|
delay = Math.max(MIN_DELAY_MS, delay);
|
||||||
|
|
||||||
cleanup();
|
attempt++;
|
||||||
|
|
||||||
// If no time left for a meaningful delay+retry, bail with lastError-like info
|
|
||||||
if (delay <= 0) {
|
|
||||||
throw new Error(`Fetch failed for ${url} after ${maxRetryTime}ms (no window left)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
attempt += 1;
|
|
||||||
await sleep(delay);
|
await sleep(delay);
|
||||||
continue;
|
continue;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
cleanup();
|
|
||||||
lastError = err;
|
lastError = err;
|
||||||
|
this?.initError?.(`Fetch error for ${url}`, err);
|
||||||
|
|
||||||
// If caller aborted, surface immediately
|
if (!retryStart) retryStart = Date.now();
|
||||||
if (callerSignal && callerSignal.aborted) {
|
|
||||||
|
if (Date.now() - retryStart >= maxRetryTime) {
|
||||||
throw lastError;
|
throw lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network/timeout errors are retryable while we have time
|
|
||||||
const nowElapsed = Date.now() - startTime;
|
|
||||||
const nowRemaining = maxRetryTime - nowElapsed;
|
|
||||||
if (nowRemaining <= 0) {
|
|
||||||
throw lastError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backoff before retrying network errors as well
|
|
||||||
const rawDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
|
const rawDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
|
||||||
const jitter = rawDelay * JITTER_FRAC;
|
const jitter = rawDelay * JITTER_FRAC;
|
||||||
let delay = rawDelay + (Math.random() * 2 * jitter - jitter);
|
let delay = rawDelay + (Math.random() * 2 * jitter - jitter);
|
||||||
delay = Math.max(MIN_DELAY_MS, Math.min(delay, nowRemaining - 1));
|
delay = Math.max(MIN_DELAY_MS, delay);
|
||||||
|
|
||||||
// If no time left to wait, throw
|
attempt++;
|
||||||
if (delay <= 0) {
|
|
||||||
throw lastError;
|
|
||||||
}
|
|
||||||
|
|
||||||
this?.initError?.(`Fetch error for ${url}`, err);
|
|
||||||
|
|
||||||
attempt += 1;
|
|
||||||
await sleep(delay);
|
await sleep(delay);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -194,6 +135,7 @@ const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [invComments, videoInfo] = await Promise.all([
|
const [invComments, videoInfo] = await Promise.all([
|
||||||
fetchWithRetry(
|
fetchWithRetry(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user