test something
This commit is contained in:
parent
f550a683fb
commit
234169fc1c
@ -59,179 +59,224 @@ class InnerTubePokeVidious {
|
|||||||
"User-Agent": this.useragent,
|
"User-Agent": this.useragent,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Retries only within a 8s window that starts AFTER the first 500/502.
|
// Retries only within a 5s window that starts AFTER the first 500/502.
|
||||||
// Fast path: one plain fetch with no extra timers/signals unless 500/502 occurs.
|
// Fast path: one plain fetch with no extra timers/signals unless 500/502 occurs.
|
||||||
const fetchWithRetry = async (url, options = {}, maxRetryTime = 8000) => {
|
const fetchWithRetry = async (url, options = {}, maxRetryTime = 5000) => {
|
||||||
let lastError;
|
let lastError;
|
||||||
|
|
||||||
// Trigger statuses that arm the retry window
|
// Trigger statuses that arm the retry window
|
||||||
const TRIGGER = 500 | 502; // bitwise trick for branch hints; DO NOT rely on value
|
const TRIGGER = 500 | 502; // bitwise trick for branch hints; DO NOT rely on value
|
||||||
const isTrigger = (s) => (s === 500 || s === 502);
|
const isTrigger = (s) => (s === 500 || s === 502);
|
||||||
|
|
||||||
// Once armed, these are retryable (plus network errors)
|
// Once armed, these are retryable (plus network errors)
|
||||||
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
|
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
|
||||||
|
|
||||||
// Backoff (decorrelated jitter) — gentle defaults
|
// Backoff (decorrelated jitter) — gentle defaults
|
||||||
const MIN_DELAY_MS = 150;
|
const MIN_DELAY_MS = 150;
|
||||||
const BASE_DELAY_MS = 250;
|
const BASE_DELAY_MS = 250;
|
||||||
const MAX_DELAY_MS = 2000;
|
const MAX_DELAY_MS = 2000;
|
||||||
const JITTER_FACTOR = 3;
|
const JITTER_FACTOR = 3;
|
||||||
|
|
||||||
// Per-attempt timeout (only used after window is armed)
|
// Per-attempt timeout (only used after window is armed)
|
||||||
const PER_TRY_TIMEOUT_MS = 2000;
|
const PER_TRY_TIMEOUT_MS = 2000;
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
// Parse Retry-After (delta-seconds or HTTP-date)
|
// Parse Retry-After (delta-seconds or HTTP-date)
|
||||||
const parseRetryAfter = (hdr) => {
|
const parseRetryAfter = (hdr) => {
|
||||||
if (!hdr) return null;
|
if (!hdr) return null;
|
||||||
const s = String(hdr).trim();
|
const s = String(hdr).trim();
|
||||||
const delta = Number(s);
|
const delta = Number(s);
|
||||||
if (Number.isFinite(delta)) return Math.max(0, (delta * 1000) | 0);
|
if (Number.isFinite(delta)) return Math.max(0, (delta * 1000) | 0);
|
||||||
const when = Date.parse(s);
|
const when = Date.parse(s);
|
||||||
if (!Number.isNaN(when)) return Math.max(0, when - Date.now());
|
if (!Number.isNaN(when)) return Math.max(0, when - Date.now());
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// FAST PATH: single plain fetch (no AbortController, no timeout, no extra work)
|
// FAST PATH: single plain fetch (no AbortController, no timeout, no extra work)
|
||||||
let res;
|
let res;
|
||||||
try {
|
try {
|
||||||
res = await fetch(url, {
|
res = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
...options?.headers,
|
...options?.headers,
|
||||||
...headers,
|
...headers,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Network error BEFORE any 500/502 trigger → surface immediately (no retries)
|
// Network error BEFORE any 500/502 trigger → surface immediately (no retries)
|
||||||
this?.initError?.(`Fetch error for ${url}`, err);
|
this?.initError?.(`Fetch error for ${url}`, err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
|
||||||
|
|
||||||
if (res.ok) return res;
|
|
||||||
|
|
||||||
// Not a trigger? return immediately (no retry window, no delays)
|
|
||||||
if (!isTrigger(res.status)) return res;
|
|
||||||
|
|
||||||
// SLOW PATH (only after a 500/502): arm the retry window
|
|
||||||
const retryStart = Date.now();
|
|
||||||
let delayMs = BASE_DELAY_MS; // backoff seed
|
|
||||||
let attempt = 1;
|
|
||||||
const callerSignal = options?.signal || null;
|
|
||||||
|
|
||||||
// Helper: one attempt with internal timeout that respects caller aborts
|
|
||||||
const attemptWithTimeout = async (timeoutMs) => {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timer = setTimeout(
|
|
||||||
() => controller.abort(new Error("Fetch attempt timed out")),
|
|
||||||
timeoutMs > 0 ? timeoutMs : 1
|
|
||||||
);
|
|
||||||
|
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
...options?.headers,
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timer);
|
|
||||||
if (callerSignal) callerSignal.removeEventListener("abort", onCallerAbort);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional short stagger before the first retry to reduce herd effects
|
|
||||||
// await sleep(50 + ((Math.random() * 150) | 0));
|
|
||||||
|
|
||||||
// Retry loop within the 8s window
|
|
||||||
while (true) {
|
|
||||||
const elapsed = Date.now() - retryStart;
|
|
||||||
const remaining = maxRetryTime - elapsed;
|
|
||||||
if (remaining <= 0) {
|
|
||||||
throw new Error(`Fetch failed for ${url} after ${maxRetryTime}ms`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const perTryTimeout = Math.min(PER_TRY_TIMEOUT_MS, Math.max(100, remaining - 50));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await attemptWithTimeout(perTryTimeout);
|
|
||||||
if (r.ok) return r;
|
|
||||||
|
|
||||||
if (!RETRYABLE.has(r.status)) {
|
|
||||||
// Non-retryable after window armed → return immediately
|
|
||||||
return r;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respect server cooldown if provided
|
if (res.ok) return res;
|
||||||
const retryAfterMs = parseRetryAfter(r.headers.get("Retry-After"));
|
|
||||||
let waitMs;
|
// Not a trigger? return immediately (no retry window, no delays)
|
||||||
if (retryAfterMs != null) {
|
if (!isTrigger(res.status)) return res;
|
||||||
waitMs = Math.max(MIN_DELAY_MS, Math.min(retryAfterMs, Math.max(0, remaining - 10)));
|
|
||||||
} else {
|
// SLOW PATH (only after a 500/502): arm the retry window
|
||||||
// Decorrelated jitter: min(MAX, random(MIN, prev*factor))
|
const retryStart = Date.now();
|
||||||
const next = Math.min(MAX_DELAY_MS, Math.random() * delayMs * JITTER_FACTOR);
|
let delayMs = BASE_DELAY_MS; // backoff seed
|
||||||
delayMs = next < MIN_DELAY_MS ? MIN_DELAY_MS : next;
|
let attempt = 1;
|
||||||
waitMs = Math.min(delayMs, Math.max(0, remaining - 10));
|
const callerSignal = options?.signal || null;
|
||||||
|
|
||||||
|
// Helper: one attempt with internal timeout that respects caller aborts
|
||||||
|
const attemptWithTimeout = async (timeoutMs) => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(
|
||||||
|
() => controller.abort(new Error("Fetch attempt timed out")),
|
||||||
|
timeoutMs > 0 ? timeoutMs : 1
|
||||||
|
);
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options?.headers,
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (callerSignal) callerSignal.removeEventListener("abort", onCallerAbort);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional short stagger before the first retry to reduce herd effects
|
||||||
|
// await sleep(50 + ((Math.random() * 150) | 0));
|
||||||
|
|
||||||
|
// Retry loop within the 5s window
|
||||||
|
while (true) {
|
||||||
|
const elapsed = Date.now() - retryStart;
|
||||||
|
const remaining = maxRetryTime - elapsed;
|
||||||
|
if (remaining <= 0) {
|
||||||
|
throw new Error(`Fetch failed for ${url} after ${maxRetryTime}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const perTryTimeout = Math.min(PER_TRY_TIMEOUT_MS, Math.max(100, remaining - 50));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await attemptWithTimeout(perTryTimeout);
|
||||||
|
if (r.ok) return r;
|
||||||
|
|
||||||
|
if (!RETRYABLE.has(r.status)) {
|
||||||
|
// Non-retryable after window armed → return immediately
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respect server cooldown if provided
|
||||||
|
const retryAfterMs = parseRetryAfter(r.headers.get("Retry-After"));
|
||||||
|
let waitMs;
|
||||||
|
if (retryAfterMs != null) {
|
||||||
|
waitMs = Math.max(MIN_DELAY_MS, Math.min(retryAfterMs, Math.max(0, remaining - 10)));
|
||||||
|
} else {
|
||||||
|
// Decorrelated jitter: min(MAX, random(MIN, prev*factor))
|
||||||
|
const next = Math.min(MAX_DELAY_MS, Math.random() * delayMs * JITTER_FACTOR);
|
||||||
|
delayMs = next < MIN_DELAY_MS ? MIN_DELAY_MS : next;
|
||||||
|
waitMs = Math.min(delayMs, Math.max(0, remaining - 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitMs <= 0) {
|
||||||
|
throw new Error(`Fetch failed for ${url} after ${maxRetryTime}ms (window depleted)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this?.initError?.(`Retrying fetch for ${url}`, r.status);
|
||||||
|
attempt++;
|
||||||
|
await sleep(waitMs);
|
||||||
|
continue;
|
||||||
|
} catch (err) {
|
||||||
|
// Caller aborted → surface immediately
|
||||||
|
if (callerSignal && callerSignal.aborted) throw err;
|
||||||
|
|
||||||
|
lastError = err;
|
||||||
|
|
||||||
|
const remaining2 = maxRetryTime - (Date.now() - retryStart);
|
||||||
|
if (remaining2 <= 0) throw lastError;
|
||||||
|
|
||||||
|
// Backoff after network/timeout errors, too
|
||||||
|
const next = Math.min(MAX_DELAY_MS, Math.random() * delayMs * JITTER_FACTOR);
|
||||||
|
delayMs = next < MIN_DELAY_MS ? MIN_DELAY_MS : next;
|
||||||
|
const waitMs = Math.min(delayMs, Math.max(0, remaining2 - 10));
|
||||||
|
if (waitMs <= 0) throw lastError;
|
||||||
|
|
||||||
|
this?.initError?.(`Fetch error for ${url}`, err);
|
||||||
|
attempt++;
|
||||||
|
await sleep(waitMs);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (waitMs <= 0) {
|
// Helper: fetch the videos endpoint but fall back to inv_fallback if primary fails or times out
|
||||||
throw new Error(`Fetch failed for ${url} after ${maxRetryTime}ms (window depleted)`);
|
const fetchVideoTextWithFallback = async (videoId, lang, region) => {
|
||||||
|
const primaryUrl = `${this.config.invapi}/videos/${videoId}?hl=${lang}®ion=${region}&h=${btoa(
|
||||||
|
Date.now()
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
// Build fallback URL carefully — allow inv_fallback to end with or without trailing slash.
|
||||||
|
// inv_fallback is expected to be something like: https://poketube.duti.dev/api/v1/videos/
|
||||||
|
const fallbackBase = (this.config.inv_fallback || "").replace(/\/+$/, "");
|
||||||
|
const fallbackUrl =
|
||||||
|
fallbackBase && fallbackBase.includes("/videos")
|
||||||
|
? `${fallbackBase.replace(/\/+$/, "")}/${videoId}`
|
||||||
|
: `${fallbackBase}/${videoId}?hl=${lang}®ion=${region}&h=${btoa(Date.now())}`;
|
||||||
|
|
||||||
|
const PRIMARY_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
// Try primary within timeout window
|
||||||
|
try {
|
||||||
|
// Race the fetchWithRetry against a timeout promise
|
||||||
|
const primaryPromise = fetchWithRetry(primaryUrl, {}, 8000);
|
||||||
|
const timeoutPromise = new Promise((_, rej) =>
|
||||||
|
setTimeout(() => rej(new Error("Primary videos API timed out")), PRIMARY_TIMEOUT_MS)
|
||||||
|
);
|
||||||
|
|
||||||
|
const r = await Promise.race([primaryPromise, timeoutPromise]);
|
||||||
|
// If we got a Response-like object, return its text (works even if r is a Response)
|
||||||
|
return await r.text();
|
||||||
|
} catch (errPrimary) {
|
||||||
|
// Primary failed or timed out — try fallback if available
|
||||||
|
this.initError("Primary videos API failed or timed out, trying fallback", errPrimary?.stack || errPrimary);
|
||||||
|
|
||||||
|
if (!fallbackBase) {
|
||||||
|
// No fallback configured; rethrow primary error
|
||||||
|
throw errPrimary;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r2 = await fetchWithRetry(fallbackUrl, {}, 8000);
|
||||||
|
// If fallback returns a Response-like object, return its text
|
||||||
|
return await r2.text();
|
||||||
|
} catch (errFallback) {
|
||||||
|
// Both failed — log and rethrow
|
||||||
|
this.initError("Fallback videos API failed", errFallback?.stack || errFallback);
|
||||||
|
throw errFallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
this?.initError?.(`Retrying fetch for ${url}`, r.status);
|
|
||||||
attempt++;
|
|
||||||
await sleep(waitMs);
|
|
||||||
continue;
|
|
||||||
} catch (err) {
|
|
||||||
// Caller aborted → surface immediately
|
|
||||||
if (callerSignal && callerSignal.aborted) throw err;
|
|
||||||
|
|
||||||
lastError = err;
|
|
||||||
|
|
||||||
const remaining2 = maxRetryTime - (Date.now() - retryStart);
|
|
||||||
if (remaining2 <= 0) throw lastError;
|
|
||||||
|
|
||||||
// Backoff after network/timeout errors, too
|
|
||||||
const next = Math.min(MAX_DELAY_MS, Math.random() * delayMs * JITTER_FACTOR);
|
|
||||||
delayMs = next < MIN_DELAY_MS ? MIN_DELAY_MS : next;
|
|
||||||
const waitMs = Math.min(delayMs, Math.max(0, remaining2 - 10));
|
|
||||||
if (waitMs <= 0) throw lastError;
|
|
||||||
|
|
||||||
this?.initError?.(`Fetch error for ${url}`, err);
|
|
||||||
attempt++;
|
|
||||||
await sleep(waitMs);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [invComments, videoInfo] = await Promise.all([
|
const [invComments, videoInfo] = await Promise.all([
|
||||||
|
// Comments: only from primary invapi, same as before
|
||||||
fetchWithRetry(
|
fetchWithRetry(
|
||||||
`${this.config.invapi}/comments/${v}?hl=${contentlang}®ion=${contentregion}&h=${btoa(
|
`${this.config.invapi}/comments/${v}?hl=${contentlang}®ion=${contentregion}&h=${btoa(
|
||||||
Date.now()
|
Date.now()
|
||||||
)}`
|
)}`
|
||||||
).then((res) => res?.text()),
|
).then((res) => res?.text()),
|
||||||
fetchWithRetry(
|
// Videos: use the wrapper that falls back after ~5s to inv_fallback
|
||||||
`${this.config.invapi}/videos/${v}?hl=${contentlang}®ion=${contentregion}&h=${btoa(
|
fetchVideoTextWithFallback(v, contentlang, contentregion),
|
||||||
Date.now()
|
|
||||||
)}`
|
|
||||||
).then((res) => res?.text()),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const comments = this.getJson(invComments);
|
const comments = this.getJson(invComments);
|
||||||
@ -276,7 +321,7 @@ const fetchWithRetry = async (url, options = {}, maxRetryTime = 8000) => {
|
|||||||
vid,
|
vid,
|
||||||
comments,
|
comments,
|
||||||
channel_uploads: " ",
|
channel_uploads: " ",
|
||||||
engagement: returnyoutubedislikesapi.engagement,
|
engagement: returnyoutubedisapi.engagement,
|
||||||
wiki: "",
|
wiki: "",
|
||||||
desc: "",
|
desc: "",
|
||||||
color,
|
color,
|
||||||
@ -308,7 +353,7 @@ const fetchWithRetry = async (url, options = {}, maxRetryTime = 8000) => {
|
|||||||
|
|
||||||
const pokeTubeApiCore = new InnerTubePokeVidious({
|
const pokeTubeApiCore = new InnerTubePokeVidious({
|
||||||
invapi: "https://invid-api.poketube.fun/bHj665PpYhUdPWuKPfZuQGoX/api/v1",
|
invapi: "https://invid-api.poketube.fun/bHj665PpYhUdPWuKPfZuQGoX/api/v1",
|
||||||
inv_fallback:"https://fallback-invid-api.poketube.fun/api/v1",
|
inv_fallback:"https://poketube.duti.dev/api/v1/videos/",
|
||||||
useragent: config.useragent,
|
useragent: config.useragent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user