Merge pull request #1544 from PR0M3TH3AN/ai/refactor-url-health-20241026-6939191548268161877

refactor: extract UrlHealthController from js/app.js
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-22 23:09:18 -05:00
commit e1cdb06784
5 files changed
+719 -410

No files matched your search

+30 -404
View File
@@ -161,6 +161,7 @@ import {
import ApplicationBootstrap from "./ui/applicationBootstrap.js";
import EngagementController from "./ui/engagementController.js";
import SimilarContentController from "./ui/similarContentController.js";
import UrlHealthController from "./ui/urlHealthController.js";
import VideoModalCommentController from "./ui/videoModalCommentController.js";
import TorrentStatusController from "./ui/torrentStatusController.js";
import ModerationActionController from "./services/moderationActionController.js";
@@ -300,6 +301,28 @@ class Application {
getProfileCacheEntry: (pubkey) => this.getProfileCacheEntry(pubkey),
});
this.urlHealthController = new UrlHealthController({
state: {
getCachedUrlHealth: (eventId, url) =>
this.getCachedUrlHealth(eventId, url),
storeUrlHealth: (eventId, url, result, ttl) =>
this.storeUrlHealth(eventId, url, result, ttl),
getInFlightUrlProbe,
setInFlightUrlProbe,
},
utils: {
updateVideoCardSourceVisibility,
},
logger: devLogger,
constants: {
URL_PROBE_TIMEOUT_MS,
urlHealthConstants,
},
callbacks: {
getVideoListView: () => this.videoListView,
},
});
this.engagementController = new EngagementController({
services: {
nostrClient,
@@ -6282,23 +6305,7 @@ class Application {
* one place avoids subtle mismatches when we tweak copy or classes later.
*/
getUrlHealthPlaceholderMarkup(options = {}) {
const includeMargin = options?.includeMargin !== false;
const classes = ["badge", "url-health-badge", "text-muted"];
if (includeMargin) {
classes.push("mt-sm");
}
return `
<span
class="${classes.join(" ")}"
data-url-health-state="checking"
data-variant="neutral"
aria-live="polite"
role="status"
>
⏳ CDN
</span>
`;
return this.urlHealthController.getUrlHealthPlaceholderMarkup(options);
}
getTorrentHealthBadgeMarkup(options = {}) {
@@ -6334,178 +6341,11 @@ class Application {
}
updateUrlHealthBadge(badgeEl, state, videoId) {
if (!badgeEl) {
return;
}
if (videoId && badgeEl.dataset.urlHealthFor && badgeEl.dataset.urlHealthFor !== videoId) {
return;
}
if (!badgeEl.isConnected) {
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => {
if (badgeEl.isConnected) {
this.updateUrlHealthBadge(badgeEl, state, videoId);
}
});
}
return;
}
const status = state?.status || "checking";
const fallbackMessages = {
healthy: "✅ CDN",
offline: "❌ CDN",
unknown: "⚠️ CDN",
timeout: "⚠️ CDN timed out",
checking: "⏳ CDN",
};
const message =
state?.message ||
fallbackMessages[status] ||
fallbackMessages.checking;
const hadCompactMargin =
badgeEl.classList.contains("mt-sm") || badgeEl.classList.contains("mt-3");
badgeEl.dataset.urlHealthState = status;
const cardEl = badgeEl.closest(".card[data-video-id]");
if (cardEl) {
cardEl.dataset.urlHealthState = status;
updateVideoCardSourceVisibility(cardEl);
}
badgeEl.setAttribute("aria-live", "polite");
badgeEl.setAttribute("role", status === "offline" ? "alert" : "status");
badgeEl.textContent = message;
const classes = ["badge", "url-health-badge"];
if (hadCompactMargin) {
classes.push("mt-sm");
}
badgeEl.className = classes.join(" ");
const variantMap = {
healthy: "success",
offline: "critical",
unknown: "neutral",
timeout: "neutral",
checking: "neutral",
};
const variant = variantMap[status];
if (variant) {
badgeEl.dataset.variant = variant;
} else if (badgeEl.dataset.variant) {
delete badgeEl.dataset.variant;
}
if (
videoId &&
this.videoListView &&
typeof this.videoListView.cacheUrlHealth === "function"
) {
this.videoListView.cacheUrlHealth(videoId, {
status,
message,
lastCheckedAt: Number.isFinite(state?.lastCheckedAt)
? state.lastCheckedAt
: undefined,
});
}
return this.urlHealthController.updateUrlHealthBadge(badgeEl, state, videoId);
}
handleUrlHealthBadge({ video, url, badgeEl }) {
if (!video?.id || !badgeEl || !url) {
return;
}
const eventId = video.id;
const trimmedUrl = typeof url === "string" ? url.trim() : "";
if (!trimmedUrl) {
return;
}
badgeEl.dataset.urlHealthFor = eventId;
const cached = this.getCachedUrlHealth(eventId, trimmedUrl);
if (cached) {
this.updateUrlHealthBadge(badgeEl, cached, eventId);
return;
}
this.updateUrlHealthBadge(badgeEl, { status: "checking" }, eventId);
const probeOptions = { confirmPlayable: true };
const existingProbe = getInFlightUrlProbe(eventId, trimmedUrl, probeOptions);
if (existingProbe) {
existingProbe
.then((entry) => {
if (entry) {
this.updateUrlHealthBadge(badgeEl, entry, eventId);
}
})
.catch((err) => {
devLogger.warn(
`[urlHealth] cached probe promise rejected for ${trimmedUrl}:`,
err
);
});
return;
}
const probePromise = this.probeUrl(trimmedUrl, probeOptions)
.then((result) => {
const outcome = result?.outcome || "error";
let entry;
if (outcome === "ok") {
entry = { status: "healthy", message: "✅ CDN" };
} else if (outcome === "opaque" || outcome === "unknown") {
entry = {
status: "unknown",
message: "⚠️ CDN",
};
} else if (outcome === "timeout") {
entry = {
status: "timeout",
message: "⚠️ CDN timed out",
};
} else {
entry = {
status: "offline",
message: "❌ CDN",
};
}
const ttlOverride =
entry.status === "timeout" || entry.status === "unknown"
? urlHealthConstants.URL_HEALTH_TIMEOUT_RETRY_MS
: undefined;
return this.storeUrlHealth(eventId, trimmedUrl, entry, ttlOverride);
})
.catch((err) => {
devLogger.warn(`[urlHealth] probe failed for ${trimmedUrl}:`, err);
const entry = {
status: "offline",
message: "❌ CDN",
};
return this.storeUrlHealth(eventId, trimmedUrl, entry);
});
setInFlightUrlProbe(eventId, trimmedUrl, probePromise, probeOptions);
probePromise
.then((entry) => {
if (entry) {
this.updateUrlHealthBadge(badgeEl, entry, eventId);
}
})
.catch((err) => {
devLogger.warn(
`[urlHealth] probe promise rejected post-cache for ${trimmedUrl}:`,
err
);
});
handleUrlHealthBadge(payload) {
return this.urlHealthController.handleUrlHealthBadge(payload);
}
handleStreamHealthBadgeUpdate(detail) {
@@ -7965,226 +7805,12 @@ class Application {
}
}
async probeUrlWithVideoElement(url, timeoutMs = URL_PROBE_TIMEOUT_MS) {
const trimmed = typeof url === "string" ? url.trim() : "";
if (!trimmed || typeof document === "undefined") {
return { outcome: "error" };
}
return new Promise((resolve) => {
const video = document.createElement("video");
let settled = false;
let timeoutId = null;
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
video.removeEventListener("loadeddata", handleSuccess);
video.removeEventListener("canplay", handleSuccess);
video.removeEventListener("error", handleError);
try {
video.pause();
} catch (err) {
// ignore pause failures (e.g. if never played)
}
try {
video.removeAttribute("src");
video.load();
} catch (err) {
// ignore cleanup failures
}
};
const settle = (result) => {
if (settled) {
return;
}
settled = true;
cleanup();
resolve(result);
};
const handleSuccess = () => {
settle({ outcome: "ok" });
};
const handleError = () => {
settle({ outcome: "error" });
};
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
timeoutId = setTimeout(() => {
settle({ outcome: "timeout" });
}, timeoutMs);
}
try {
video.preload = "metadata";
video.muted = true;
video.playsInline = true;
video.addEventListener("loadeddata", handleSuccess, { once: true });
video.addEventListener("canplay", handleSuccess, { once: true });
video.addEventListener("error", handleError, { once: true });
video.src = trimmed;
video.load();
} catch (err) {
settle({ outcome: "error", error: err });
}
});
async probeUrlWithVideoElement(url, timeoutMs) {
return this.urlHealthController.probeUrlWithVideoElement(url, timeoutMs);
}
async probeUrl(url, options = {}) {
const trimmed = typeof url === "string" ? url.trim() : "";
if (!trimmed) {
return { outcome: "invalid" };
}
const confirmPlayable = options?.confirmPlayable === true;
const confirmWithVideoElement = async () => {
if (!confirmPlayable) {
return null;
}
const initialTimeout =
Number.isFinite(options?.videoProbeTimeoutMs) &&
options.videoProbeTimeoutMs > 0
? options.videoProbeTimeoutMs
: URL_PROBE_TIMEOUT_MS;
const attemptWithTimeout = async (timeoutMs) => {
try {
const result = await this.probeUrlWithVideoElement(trimmed, timeoutMs);
if (result && result.outcome) {
return result;
}
} catch (err) {
devLogger.warn(
`[probeUrl] Video element probe threw for ${trimmed}:`,
err
);
}
return null;
};
let result = await attemptWithTimeout(initialTimeout);
if (
result &&
result.outcome === "timeout" &&
Number.isFinite(urlHealthConstants.URL_PROBE_TIMEOUT_RETRY_MS) &&
urlHealthConstants.URL_PROBE_TIMEOUT_RETRY_MS > initialTimeout
) {
const retryResult = await attemptWithTimeout(
urlHealthConstants.URL_PROBE_TIMEOUT_RETRY_MS
);
if (retryResult) {
result = { ...retryResult, retriedAfterTimeout: true };
}
}
return result;
};
const supportsAbort = typeof AbortController !== "undefined";
const controller = supportsAbort ? new AbortController() : null;
let timeoutId = null;
const racers = [
fetch(trimmed, {
method: "HEAD",
mode: "no-cors",
cache: "no-store",
signal: controller ? controller.signal : undefined,
}),
];
if (Number.isFinite(URL_PROBE_TIMEOUT_MS) && URL_PROBE_TIMEOUT_MS > 0) {
racers.push(
new Promise((resolve) => {
timeoutId = setTimeout(() => {
if (controller) {
try {
controller.abort();
} catch (err) {
// ignore abort errors
}
}
resolve({ outcome: "timeout" });
}, URL_PROBE_TIMEOUT_MS);
})
);
}
let responseOrTimeout;
try {
responseOrTimeout = await Promise.race(racers);
} catch (err) {
if (timeoutId) {
clearTimeout(timeoutId);
}
devLogger.warn(`[probeUrl] HEAD request failed for ${trimmed}:`, err);
const fallback = await confirmWithVideoElement();
if (fallback) {
return fallback;
}
return confirmPlayable
? { outcome: "error", error: err }
: { outcome: "unknown", error: err };
}
if (timeoutId) {
clearTimeout(timeoutId);
}
if (responseOrTimeout && responseOrTimeout.outcome === "timeout") {
const fallback = await confirmWithVideoElement();
if (fallback) {
return fallback;
}
return confirmPlayable ? { outcome: "timeout" } : { outcome: "unknown" };
}
const response = responseOrTimeout;
if (!response) {
return { outcome: "error" };
}
if (response.type === "opaque") {
const fallback = await confirmWithVideoElement();
if (fallback) {
return fallback;
}
return { outcome: confirmPlayable ? "opaque" : "unknown" };
}
if (!response.ok) {
const fallback = await confirmWithVideoElement();
if (fallback) {
return fallback;
}
return {
outcome: "bad",
status: response.status,
};
}
const playbackCheck = await confirmWithVideoElement();
if (playbackCheck) {
if (playbackCheck.outcome === "ok") {
return {
...playbackCheck,
status: response.status,
};
}
return playbackCheck;
}
return {
outcome: "ok",
status: response.status,
};
return this.urlHealthController.probeUrl(url, options);
}
async playHttp(videoEl, url) {
+7 -2
View File
@@ -158,7 +158,7 @@ function normalizeResult(result) {
};
}
function queueProbe(magnet, cacheKey, priority = 0) {
function queueProbe(magnet, cacheKey, priority = 0, webSeeds = []) {
if (!magnet) {
return Promise.resolve(null);
}
@@ -180,6 +180,7 @@ function queueProbe(magnet, cacheKey, priority = 0) {
timeoutMs: PROBE_TIMEOUT_MS,
maxWebConns: 2,
polls: PROBE_POLL_COUNT,
urlList: webSeeds,
})
.catch((err) => ({
healthy: false,
@@ -584,7 +585,11 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) {
setBadge(card, "checking");
const probePromise = queueProbe(magnet, infoHash, priority);
const encodedUrl = card.dataset.urlHealthUrl || "";
const webSeed = encodedUrl ? decodeURIComponent(encodedUrl) : "";
const webSeeds = webSeed ? [webSeed] : [];
const probePromise = queueProbe(magnet, infoHash, priority, webSeeds);
pendingByCard.set(card, probePromise);
+477
View File
@@ -0,0 +1,477 @@
export default class UrlHealthController {
constructor({
state = {},
utils = {},
logger = {},
constants = {},
callbacks = {},
} = {}) {
this.state = state;
this.utils = utils;
this.logger = logger;
this.constants = constants;
this.callbacks = callbacks;
}
getUrlHealthPlaceholderMarkup(options = {}) {
const includeMargin = options?.includeMargin !== false;
const classes = ["badge", "url-health-badge", "text-muted"];
if (includeMargin) {
classes.push("mt-sm");
}
return `
<span
class="${classes.join(" ")}"
data-url-health-state="checking"
data-variant="neutral"
aria-live="polite"
role="status"
>
⏳ CDN
</span>
`;
}
updateUrlHealthBadge(badgeEl, state, videoId) {
if (!badgeEl) {
return;
}
if (
videoId &&
badgeEl.dataset.urlHealthFor &&
badgeEl.dataset.urlHealthFor !== videoId
) {
return;
}
if (!badgeEl.isConnected) {
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => {
if (badgeEl.isConnected) {
this.updateUrlHealthBadge(badgeEl, state, videoId);
}
});
}
return;
}
const status = state?.status || "checking";
const fallbackMessages = {
healthy: "✅ CDN",
offline: "❌ CDN",
unknown: "⚠️ CDN",
timeout: "⚠️ CDN timed out",
checking: "⏳ CDN",
};
const message =
state?.message ||
fallbackMessages[status] ||
fallbackMessages.checking;
const hadCompactMargin =
badgeEl.classList.contains("mt-sm") || badgeEl.classList.contains("mt-3");
badgeEl.dataset.urlHealthState = status;
const cardEl = badgeEl.closest(".card[data-video-id]");
if (cardEl && typeof this.utils.updateVideoCardSourceVisibility === "function") {
cardEl.dataset.urlHealthState = status;
this.utils.updateVideoCardSourceVisibility(cardEl);
}
badgeEl.setAttribute("aria-live", "polite");
badgeEl.setAttribute("role", status === "offline" ? "alert" : "status");
badgeEl.textContent = message;
const classes = ["badge", "url-health-badge"];
if (hadCompactMargin) {
classes.push("mt-sm");
}
badgeEl.className = classes.join(" ");
const variantMap = {
healthy: "success",
offline: "critical",
unknown: "neutral",
timeout: "neutral",
checking: "neutral",
};
const variant = variantMap[status];
if (variant) {
badgeEl.dataset.variant = variant;
} else if (badgeEl.dataset.variant) {
delete badgeEl.dataset.variant;
}
const videoListView =
this.callbacks.getVideoListView &&
typeof this.callbacks.getVideoListView === "function"
? this.callbacks.getVideoListView()
: null;
if (
videoId &&
videoListView &&
typeof videoListView.cacheUrlHealth === "function"
) {
videoListView.cacheUrlHealth(videoId, {
status,
message,
lastCheckedAt: Number.isFinite(state?.lastCheckedAt)
? state.lastCheckedAt
: undefined,
});
}
}
handleUrlHealthBadge({ video, url, badgeEl }) {
if (!video?.id || !badgeEl || !url) {
return;
}
const eventId = video.id;
const trimmedUrl = typeof url === "string" ? url.trim() : "";
if (!trimmedUrl) {
return;
}
badgeEl.dataset.urlHealthFor = eventId;
if (typeof this.state.getCachedUrlHealth === "function") {
const cached = this.state.getCachedUrlHealth(eventId, trimmedUrl);
if (cached) {
this.updateUrlHealthBadge(badgeEl, cached, eventId);
return;
}
}
this.updateUrlHealthBadge(badgeEl, { status: "checking" }, eventId);
const probeOptions = { confirmPlayable: true };
const getInFlight = this.state.getInFlightUrlProbe;
const existingProbe =
typeof getInFlight === "function"
? getInFlight(eventId, trimmedUrl, probeOptions)
: null;
if (existingProbe) {
existingProbe
.then((entry) => {
if (entry) {
this.updateUrlHealthBadge(badgeEl, entry, eventId);
}
})
.catch((err) => {
this.logger.warn(
`[urlHealth] cached probe promise rejected for ${trimmedUrl}:`,
err
);
});
return;
}
const probePromise = this.probeUrl(trimmedUrl, probeOptions)
.then((result) => {
const outcome = result?.outcome || "error";
let entry;
if (outcome === "ok") {
entry = { status: "healthy", message: "✅ CDN" };
} else if (outcome === "opaque" || outcome === "unknown") {
entry = {
status: "unknown",
message: "⚠️ CDN",
};
} else if (outcome === "timeout") {
entry = {
status: "timeout",
message: "⚠️ CDN timed out",
};
} else {
entry = {
status: "offline",
message: "❌ CDN",
};
}
const urlHealthConstants = this.constants.urlHealthConstants || {};
const ttlOverride =
entry.status === "timeout" || entry.status === "unknown"
? urlHealthConstants.URL_HEALTH_TIMEOUT_RETRY_MS
: undefined;
if (typeof this.state.storeUrlHealth === "function") {
return this.state.storeUrlHealth(
eventId,
trimmedUrl,
entry,
ttlOverride
);
}
return entry;
})
.catch((err) => {
this.logger.warn(`[urlHealth] probe failed for ${trimmedUrl}:`, err);
const entry = {
status: "offline",
message: "❌ CDN",
};
if (typeof this.state.storeUrlHealth === "function") {
return this.state.storeUrlHealth(eventId, trimmedUrl, entry);
}
return entry;
});
if (typeof this.state.setInFlightUrlProbe === "function") {
this.state.setInFlightUrlProbe(
eventId,
trimmedUrl,
probePromise,
probeOptions
);
}
probePromise
.then((entry) => {
if (entry) {
this.updateUrlHealthBadge(badgeEl, entry, eventId);
}
})
.catch((err) => {
this.logger.warn(
`[urlHealth] probe promise rejected post-cache for ${trimmedUrl}:`,
err
);
});
}
async probeUrlWithVideoElement(url, timeoutMs) {
const defaultTimeout = this.constants.URL_PROBE_TIMEOUT_MS || 5000;
const effectiveTimeout =
Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : defaultTimeout;
const trimmed = typeof url === "string" ? url.trim() : "";
if (!trimmed || typeof document === "undefined") {
return { outcome: "error" };
}
return new Promise((resolve) => {
const video = document.createElement("video");
let settled = false;
let timeoutId = null;
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
video.removeEventListener("loadeddata", handleSuccess);
video.removeEventListener("canplay", handleSuccess);
video.removeEventListener("error", handleError);
try {
video.pause();
} catch (err) {
// ignore pause failures
}
try {
video.removeAttribute("src");
video.load();
} catch (err) {
// ignore cleanup failures
}
};
const settle = (result) => {
if (settled) {
return;
}
settled = true;
cleanup();
resolve(result);
};
const handleSuccess = () => {
settle({ outcome: "ok" });
};
const handleError = () => {
settle({ outcome: "error" });
};
if (Number.isFinite(effectiveTimeout) && effectiveTimeout > 0) {
timeoutId = setTimeout(() => {
settle({ outcome: "timeout" });
}, effectiveTimeout);
}
try {
video.preload = "metadata";
video.muted = true;
video.playsInline = true;
video.addEventListener("loadeddata", handleSuccess, { once: true });
video.addEventListener("canplay", handleSuccess, { once: true });
video.addEventListener("error", handleError, { once: true });
video.src = trimmed;
video.load();
} catch (err) {
settle({ outcome: "error", error: err });
}
});
}
async probeUrl(url, options = {}) {
const trimmed = typeof url === "string" ? url.trim() : "";
if (!trimmed) {
return { outcome: "invalid" };
}
const confirmPlayable = options?.confirmPlayable === true;
const defaultTimeout = this.constants.URL_PROBE_TIMEOUT_MS || 5000;
const urlHealthConstants = this.constants.urlHealthConstants || {};
const confirmWithVideoElement = async () => {
if (!confirmPlayable) {
return null;
}
const initialTimeout =
Number.isFinite(options?.videoProbeTimeoutMs) &&
options.videoProbeTimeoutMs > 0
? options.videoProbeTimeoutMs
: defaultTimeout;
const attemptWithTimeout = async (timeoutMs) => {
try {
const result = await this.probeUrlWithVideoElement(
trimmed,
timeoutMs
);
if (result && result.outcome) {
return result;
}
} catch (err) {
this.logger.warn(
`[probeUrl] Video element probe threw for ${trimmed}:`,
err
);
}
return null;
};
let result = await attemptWithTimeout(initialTimeout);
if (
result &&
result.outcome === "timeout" &&
Number.isFinite(urlHealthConstants.URL_PROBE_TIMEOUT_RETRY_MS) &&
urlHealthConstants.URL_PROBE_TIMEOUT_RETRY_MS > initialTimeout
) {
const retryResult = await attemptWithTimeout(
urlHealthConstants.URL_PROBE_TIMEOUT_RETRY_MS
);
if (retryResult) {
result = { ...retryResult, retriedAfterTimeout: true };
}
}
return result;
};
const supportsAbort = typeof AbortController !== "undefined";
const controller = supportsAbort ? new AbortController() : null;
let timeoutId = null;
const racers = [
fetch(trimmed, {
method: "HEAD",
mode: "no-cors",
cache: "no-store",
signal: controller ? controller.signal : undefined,
}),
];
if (Number.isFinite(defaultTimeout) && defaultTimeout > 0) {
racers.push(
new Promise((resolve) => {
timeoutId = setTimeout(() => {
if (controller) {
try {
controller.abort();
} catch (err) {
// ignore abort errors
}
}
resolve({ outcome: "timeout" });
}, defaultTimeout);
})
);
}
let responseOrTimeout;
try {
responseOrTimeout = await Promise.race(racers);
} catch (err) {
if (timeoutId) {
clearTimeout(timeoutId);
}
this.logger.warn(`[probeUrl] HEAD request failed for ${trimmed}:`, err);
const fallback = await confirmWithVideoElement();
if (fallback) {
return fallback;
}
return confirmPlayable
? { outcome: "error", error: err }
: { outcome: "unknown", error: err };
}
if (timeoutId) {
clearTimeout(timeoutId);
}
if (responseOrTimeout && responseOrTimeout.outcome === "timeout") {
const fallback = await confirmWithVideoElement();
if (fallback) {
return fallback;
}
return confirmPlayable ? { outcome: "timeout" } : { outcome: "unknown" };
}
const response = responseOrTimeout;
if (!response) {
return { outcome: "error" };
}
if (response.type === "opaque") {
const fallback = await confirmWithVideoElement();
if (fallback) {
return fallback;
}
return { outcome: confirmPlayable ? "opaque" : "unknown" };
}
if (!response.ok) {
const fallback = await confirmWithVideoElement();
if (fallback) {
return fallback;
}
return {
outcome: "bad",
status: response.status,
};
}
const playbackCheck = await confirmWithVideoElement();
if (playbackCheck) {
if (playbackCheck.outcome === "ok") {
return {
...playbackCheck,
status: response.status,
};
}
return playbackCheck;
}
return {
outcome: "ok",
status: response.status,
};
}
}
+10 -4
View File
@@ -175,7 +175,7 @@ export class TorrentClient {
*/
async probePeers(
magnetURI,
{ timeoutMs = 8000, maxWebConns = 2, polls = 3 } = {}
{ timeoutMs = 8000, maxWebConns = 2, polls = 3, urlList = [] } = {}
) {
const magnet = typeof magnetURI === "string" ? magnetURI.trim() : "";
if (!magnet) {
@@ -194,7 +194,9 @@ export class TorrentClient {
const { magnet: augmentedMagnet, appended, hasProbeTrackers } =
appendProbeTrackers(magnet, trackers);
const hasWebSeed = magnet.includes("ws=") || magnet.includes("webSeed=");
const hasMagnetWebSeed = magnet.includes("ws=") || magnet.includes("webSeed=");
const hasExplicitWebSeed = Array.isArray(urlList) && urlList.length > 0;
const hasWebSeed = hasMagnetWebSeed || hasExplicitWebSeed;
if (!hasProbeTrackers && !hasWebSeed) {
return {
@@ -270,10 +272,14 @@ export class TorrentClient {
};
try {
torrent = client.add(augmentedMagnet, {
const addOptions = {
announce: trackers,
maxWebConns: safeMaxWebConns,
});
};
if (hasExplicitWebSeed) {
addOptions.urlList = urlList;
}
torrent = client.add(augmentedMagnet, addOptions);
} catch (err) {
finalize({
reason: "error",
+195
View File
@@ -0,0 +1,195 @@
import test, { beforeEach, afterEach, describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { JSDOM } from 'jsdom';
import UrlHealthController from '../../js/ui/urlHealthController.js';
let dom;
let controller;
let mockCache = new Map();
let inFlightProbes = new Map();
const mockState = {
getCachedUrlHealth: (eventId, url) => mockCache.get(`${eventId}-${url}`),
storeUrlHealth: (eventId, url, result) => {
mockCache.set(`${eventId}-${url}`, result);
return result;
},
getInFlightUrlProbe: (eventId, url) => inFlightProbes.get(`${eventId}-${url}`),
setInFlightUrlProbe: (eventId, url, promise) => {
inFlightProbes.set(`${eventId}-${url}`, promise);
},
URL_PROBE_TIMEOUT_MS: 50,
urlHealthConstants: { URL_PROBE_TIMEOUT_RETRY_MS: 50 },
};
const mockLogger = {
warn: () => {},
log: () => {},
error: () => {},
};
const mockUtils = {
updateVideoCardSourceVisibility: () => {},
};
const mockCallbacks = {
getVideoListView: () => ({
cacheUrlHealth: () => {},
}),
};
beforeEach(() => {
dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: 'https://example.com',
});
global.window = dom.window;
global.document = dom.window.document;
global.HTMLElement = dom.window.HTMLElement;
global.AbortController = dom.window.AbortController;
// Mock fetch
global.fetch = async (url) => {
if (url.includes('timeout')) {
await new Promise(resolve => setTimeout(resolve, 100));
return { ok: false, status: 408 };
}
if (url.includes('404')) {
return { ok: false, status: 404 };
}
if (url.includes('error')) {
throw new Error('Network error');
}
if (url.includes('opaque')) {
return { type: 'opaque', ok: false, status: 0 };
}
return { ok: true, status: 200, type: 'basic' };
};
mockCache.clear();
inFlightProbes.clear();
controller = new UrlHealthController({
state: mockState,
utils: mockUtils,
logger: mockLogger,
constants: { URL_PROBE_TIMEOUT_MS: 50 },
callbacks: mockCallbacks,
});
});
afterEach(() => {
delete global.window;
delete global.document;
delete global.HTMLElement;
delete global.fetch;
delete global.AbortController;
});
describe('UrlHealthController', () => {
it('probeUrl returns ok for valid URL', async () => {
// Mock video element behavior
const originalCreateElement = document.createElement;
document.createElement = (tagName) => {
if (tagName === 'video') {
return {
addEventListener: (event, handler) => {
if (event === 'loadeddata') {
setTimeout(handler, 10);
}
},
removeEventListener: () => {},
load: () => {},
pause: () => {},
removeAttribute: () => {},
isConnected: false,
};
}
return originalCreateElement(tagName);
};
const result = await controller.probeUrl('https://example.com/video.mp4', { confirmPlayable: true });
assert.equal(result.outcome, 'ok');
assert.equal(result.status, 200);
document.createElement = originalCreateElement;
});
it('probeUrl returns error for 404', async () => {
// Mock video element behavior to fail
const originalCreateElement = document.createElement;
document.createElement = (tagName) => {
if (tagName === 'video') {
return {
addEventListener: (event, handler) => {
if (event === 'error') {
setTimeout(handler, 10);
}
},
removeEventListener: () => {},
load: () => {},
pause: () => {},
removeAttribute: () => {},
isConnected: false,
};
}
return originalCreateElement(tagName);
};
const result = await controller.probeUrl('https://example.com/404.mp4', { confirmPlayable: true });
assert.ok(result.outcome === 'bad' || result.outcome === 'error');
document.createElement = originalCreateElement;
});
it('handleUrlHealthBadge updates badge', async () => {
const badgeEl = document.createElement('span');
document.body.appendChild(badgeEl); // Ensure isConnected is true
badgeEl.dataset.urlHealthUrl = 'https://example.com/video.mp4';
badgeEl.dataset.urlHealthEventId = 'event1';
// Mock successful probe via video element
const originalCreateElement = document.createElement;
document.createElement = (tagName) => {
if (tagName === 'video') {
return {
addEventListener: (event, handler) => {
if (event === 'loadeddata') {
setTimeout(handler, 0);
}
},
removeEventListener: () => {},
load: () => {},
pause: () => {},
removeAttribute: () => {},
isConnected: false,
};
}
return originalCreateElement(tagName);
};
await controller.handleUrlHealthBadge({
video: { id: 'event1' },
url: 'https://example.com/video.mp4',
badgeEl
});
// Since handleUrlHealthBadge sets an in-flight probe, we might need to wait or verify the state.
// The implementation of handleUrlHealthBadge awaits internally if not careful, or just triggers logic.
// In js/app.js, it sets the in-flight probe and attaches .then() handlers.
// We need to wait for microtasks.
await new Promise(resolve => setTimeout(resolve, 50));
assert.equal(badgeEl.dataset.urlHealthState, 'healthy');
assert.equal(badgeEl.textContent, '✅ CDN');
document.createElement = originalCreateElement;
});
it('getUrlHealthPlaceholderMarkup returns string', () => {
const markup = controller.getUrlHealthPlaceholderMarkup();
assert.ok(typeof markup === 'string');
assert.ok(markup.includes('CDN'));
});
});