From babf73d777e19f3c0c4c7c7b008d130894809d3e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 03:32:28 +0000 Subject: [PATCH 1/2] refactor(ai): extract UrlHealthController from js/app.js (agent) --- js/app.js | 434 ++------------------- js/ui/urlHealthController.js | 477 ++++++++++++++++++++++++ tests/ui/url-health-controller.test.mjs | 195 ++++++++++ 3 files changed, 702 insertions(+), 404 deletions(-) create mode 100644 js/ui/urlHealthController.js create mode 100644 tests/ui/url-health-controller.test.mjs diff --git a/js/app.js b/js/app.js index d3960fa9..5dcfbe1d 100644 --- a/js/app.js +++ b/js/app.js @@ -159,6 +159,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"; @@ -298,6 +299,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, @@ -6279,23 +6302,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 ` - - ⏳ CDN - - `; + return this.urlHealthController.getUrlHealthPlaceholderMarkup(options); } getTorrentHealthBadgeMarkup(options = {}) { @@ -6331,178 +6338,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) { @@ -7962,226 +7802,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) { diff --git a/js/ui/urlHealthController.js b/js/ui/urlHealthController.js new file mode 100644 index 00000000..4591f095 --- /dev/null +++ b/js/ui/urlHealthController.js @@ -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 ` + + ⏳ CDN + + `; + } + + 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, + }; + } +} diff --git a/tests/ui/url-health-controller.test.mjs b/tests/ui/url-health-controller.test.mjs new file mode 100644 index 00000000..84f1bd1b --- /dev/null +++ b/tests/ui/url-health-controller.test.mjs @@ -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('', { + 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')); + }); +}); From 215ff29f1a76a87c9c6922cb42fe6853c5805e95 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 04:06:11 +0000 Subject: [PATCH 2/2] fix(webtorrent): use web seeds in health probe --- js/gridHealth.js | 9 +++++++-- js/webtorrent.js | 14 ++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/js/gridHealth.js b/js/gridHealth.js index 6fe39850..7b814ee9 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -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); diff --git a/js/webtorrent.js b/js/webtorrent.js index 0bb839eb..f3cf7a52 100644 --- a/js/webtorrent.js +++ b/js/webtorrent.js @@ -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",