retry window is armed ONLY after seeing a 500 or 502 response + format file
This commit is contained in:
parent
b6684cf08a
commit
586732f15b
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Poke is a Free/Libre youtube front-end !
|
* Poke is a Free/Libre YouTube front-end!
|
||||||
*
|
*
|
||||||
* This file is Licensed under LGPL-3.0-or-later. Poketube itself is GPL, Only this file is LGPL.
|
* This file is Licensed under LGPL-3.0-or-later. Poketube itself is GPL, only this file is LGPL.
|
||||||
* See a copy here: https://www.gnu.org/licenses/lgpl-3.0.txt
|
* See a copy here: https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
* Please don't remove this comment while sharing this code.
|
* Please don't remove this comment while sharing this code.
|
||||||
*/
|
*/
|
||||||
@ -50,7 +50,7 @@ class InnerTubePokeVidious {
|
|||||||
return { error: true, message: "No video ID provided" };
|
return { error: true, message: "No video ID provided" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If cached result exists and is less than 1 hour old, return it
|
// If cached result exists and is less than 1 hour old, return it
|
||||||
if (this.cache[v] && Date.now() - this.cache[v].timestamp < 3600000) {
|
if (this.cache[v] && Date.now() - this.cache[v].timestamp < 3600000) {
|
||||||
return this.cache[v].result;
|
return this.cache[v].result;
|
||||||
}
|
}
|
||||||
@ -59,82 +59,169 @@ 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 window is armed ONLY after seeing a 500 or 502 response
|
||||||
const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
||||||
let lastError;
|
let lastError;
|
||||||
|
|
||||||
// retryable HTTP statuses
|
const RETRY_WINDOW_TRIGGER = new Set([500, 502]);
|
||||||
const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
|
const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
|
||||||
|
|
||||||
// backoff settings
|
const BASE_DELAY_MS = 120;
|
||||||
const BASE_DELAY_MS = 120;
|
const MAX_DELAY_MS = 1000;
|
||||||
const MAX_DELAY_MS = 1000;
|
const MIN_DELAY_MS = 50;
|
||||||
const MIN_DELAY_MS = 50;
|
const JITTER_FRAC = 0.2;
|
||||||
const JITTER_FRAC = 0.2;
|
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const DEFAULT_PER_TRY_TIMEOUT = 2000;
|
||||||
|
|
||||||
let attempt = 0;
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
let retryStart = null;
|
|
||||||
|
|
||||||
while (true) {
|
const callerSignal = options?.signal || null;
|
||||||
try {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
...options.headers,
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
const attemptWithTimeout = async (timeoutMs) => {
|
||||||
return res; // success: return immediately
|
const controller = new AbortController();
|
||||||
}
|
const timer = setTimeout(
|
||||||
|
() => controller.abort(new Error("Fetch attempt timed out")),
|
||||||
|
Math.max(1, timeoutMs)
|
||||||
|
);
|
||||||
|
|
||||||
// only start timing if we need to retry
|
const onCallerAbort = () =>
|
||||||
if (!retryStart) retryStart = Date.now();
|
controller.abort(
|
||||||
|
callerSignal.reason || new Error("Aborted by caller")
|
||||||
|
);
|
||||||
|
|
||||||
if (!RETRYABLE_STATUS.has(res.status)) {
|
if (callerSignal) {
|
||||||
return res; // non-retryable status, return immediately
|
if (callerSignal.aborted) {
|
||||||
}
|
controller.abort(
|
||||||
|
callerSignal.reason || new Error("Aborted by caller")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
callerSignal.addEventListener("abort", onCallerAbort, {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this?.initError?.(`Retrying fetch for ${url}`, res.status);
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options?.headers,
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (callerSignal)
|
||||||
|
callerSignal.removeEventListener("abort", onCallerAbort);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (Date.now() - retryStart >= maxRetryTime) {
|
// First attempt (no retry window yet)
|
||||||
throw new Error(`Fetch failed for ${url} after ${maxRetryTime}ms`);
|
try {
|
||||||
}
|
const firstRes = await attemptWithTimeout(DEFAULT_PER_TRY_TIMEOUT);
|
||||||
|
if (firstRes.ok) return firstRes;
|
||||||
|
|
||||||
const rawDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
|
if (!RETRY_WINDOW_TRIGGER.has(firstRes.status)) {
|
||||||
const jitter = rawDelay * JITTER_FRAC;
|
return firstRes;
|
||||||
let delay = rawDelay + (Math.random() * 2 * jitter - jitter);
|
}
|
||||||
delay = Math.max(MIN_DELAY_MS, delay);
|
|
||||||
|
|
||||||
attempt++;
|
lastError = new Error(`Initial ${firstRes.status} from ${url}`);
|
||||||
await sleep(delay);
|
} catch (err) {
|
||||||
continue;
|
lastError = err;
|
||||||
} catch (err) {
|
this?.initError?.(`Fetch error for ${url}`, err);
|
||||||
lastError = err;
|
|
||||||
this?.initError?.(`Fetch error for ${url}`, err);
|
|
||||||
|
|
||||||
if (!retryStart) retryStart = Date.now();
|
|
||||||
|
|
||||||
if (Date.now() - retryStart >= maxRetryTime) {
|
|
||||||
throw lastError;
|
throw lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
|
// Retry loop: window is ARMED due to 500/502
|
||||||
const jitter = rawDelay * JITTER_FRAC;
|
const retryStart = Date.now();
|
||||||
let delay = rawDelay + (Math.random() * 2 * jitter - jitter);
|
let attempt = 1;
|
||||||
delay = Math.max(MIN_DELAY_MS, delay);
|
|
||||||
|
|
||||||
attempt++;
|
while (true) {
|
||||||
await sleep(delay);
|
const elapsed = Date.now() - retryStart;
|
||||||
continue;
|
const remaining = maxRetryTime - elapsed;
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
throw (
|
||||||
|
lastError ||
|
||||||
|
new Error(`Fetch failed for ${url} after ${maxRetryTime}ms`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const perTryTimeout = Math.max(
|
||||||
|
50,
|
||||||
|
Math.min(DEFAULT_PER_TRY_TIMEOUT, remaining - 25)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await attemptWithTimeout(perTryTimeout);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RETRYABLE_STATUS.has(res.status)) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
this?.initError?.(`Retrying fetch for ${url}`, res.status);
|
||||||
|
|
||||||
|
const raw = Math.min(
|
||||||
|
MAX_DELAY_MS,
|
||||||
|
BASE_DELAY_MS * Math.pow(2, attempt)
|
||||||
|
);
|
||||||
|
const jitter = raw * JITTER_FRAC;
|
||||||
|
let delay = raw + (Math.random() * 2 * jitter - jitter);
|
||||||
|
delay = Math.max(
|
||||||
|
MIN_DELAY_MS,
|
||||||
|
Math.min(delay, Math.max(0, remaining - 10))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (delay <= 0) {
|
||||||
|
lastError = new Error(
|
||||||
|
`Fetch failed for ${url} after ${maxRetryTime}ms (no window left)`
|
||||||
|
);
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
attempt += 1;
|
||||||
|
await sleep(delay);
|
||||||
|
continue;
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
|
||||||
|
if (callerSignal && callerSignal.aborted) {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowRemaining = maxRetryTime - (Date.now() - retryStart);
|
||||||
|
if (nowRemaining <= 0) {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = Math.min(
|
||||||
|
MAX_DELAY_MS,
|
||||||
|
BASE_DELAY_MS * Math.pow(2, attempt)
|
||||||
|
);
|
||||||
|
const jitter = raw * JITTER_FRAC;
|
||||||
|
let delay = raw + (Math.random() * 2 * jitter - jitter);
|
||||||
|
delay = Math.max(
|
||||||
|
MIN_DELAY_MS,
|
||||||
|
Math.min(delay, Math.max(0, nowRemaining - 10))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (delay <= 0) {
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
this?.initError?.(`Fetch error for ${url}`, err);
|
||||||
|
attempt += 1;
|
||||||
|
await sleep(delay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [invComments, videoInfo] = await Promise.all([
|
const [invComments, videoInfo] = await Promise.all([
|
||||||
@ -157,7 +244,8 @@ const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
|||||||
this.initError("Video info missing/unparsable", v);
|
this.initError("Video info missing/unparsable", v);
|
||||||
return {
|
return {
|
||||||
error: true,
|
error: true,
|
||||||
message: "Sorry nya, we couldn't find any information about that video qwq",
|
message:
|
||||||
|
"Sorry nya, we couldn't find any information about that video qwq",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,7 +260,9 @@ const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
|||||||
let color = "#0ea5e9";
|
let color = "#0ea5e9";
|
||||||
let color2 = "#111827";
|
let color2 = "#111827";
|
||||||
try {
|
try {
|
||||||
// `sqp` is a URL parameter used by YouTube thumbnail/image servers to request a specific scale, crop or quality profile (base64-encoded), controlling how the thumbnail is sized or compressed.
|
// `sqp` is a URL parameter used by YouTube thumbnail/image servers
|
||||||
|
// to request a specific scale, crop or quality profile (base64-encoded),
|
||||||
|
// controlling how the thumbnail is sized or compressed.
|
||||||
const palette = await getColors(
|
const palette = await getColors(
|
||||||
`https://i.ytimg.com/vi/${v}/hqdefault.jpg?sqp=${this.sqp}`
|
`https://i.ytimg.com/vi/${v}/hqdefault.jpg?sqp=${this.sqp}`
|
||||||
);
|
);
|
||||||
@ -200,7 +290,7 @@ const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
|||||||
|
|
||||||
return this.cache[v].result;
|
return this.cache[v].result;
|
||||||
} else {
|
} else {
|
||||||
this.initError( vid,`ID: ${v}` );
|
this.initError(vid, `ID: ${v}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.initError(`Error getting video ${v}`, error);
|
this.initError(`Error getting video ${v}`, error);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user