diff --git a/js/gridHealth.js b/js/gridHealth.js index a5671310..0a5c8203 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -135,9 +135,9 @@ function normalizeResult(result) { const peers = Number.isFinite(result.peers) ? Math.max(0, Number(result.peers)) : 0; - const webseedOnly = Boolean(result.webseedOnly) && peers === 0; - const healthy = (Boolean(result.healthy) && peers > 0) || webseedOnly; + const healthy = Boolean(result.healthy) && peers > 0; const reason = typeof result.reason === "string" ? result.reason : "error"; + const webseedOnly = Boolean(result.webseedOnly) && peers === 0; return { healthy, peers: healthy ? Math.max(1, peers) : peers, @@ -603,8 +603,7 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) { setBadge(card, "unknown"); return; } - const isWebseedOnly = result.webseedOnly || result.reason === "webseed"; - if (!result.hasProbeTrackers && !isWebseedOnly) { + if (!result.hasProbeTrackers) { setBadge(card, "unknown", result); return; } diff --git a/js/webtorrent.js b/js/webtorrent.js index ac0617fe..84dbaf38 100644 --- a/js/webtorrent.js +++ b/js/webtorrent.js @@ -23,33 +23,9 @@ import { WSS_TRACKERS } from "./constants.js"; import { safeDecodeURIComponent } from "./utils/safeDecode.js"; import { devLogger, userLogger } from "./utils/logger.js"; import { emit } from "./embedDiagnostics.js"; -import { infoHashFromMagnet } from "./magnets.js"; const DEFAULT_PROBE_TRACKERS = Object.freeze([...WSS_TRACKERS]); -function stripXsParameter(magnetURI) { - const trimmed = typeof magnetURI === "string" ? magnetURI.trim() : ""; - if ( - typeof window !== "undefined" && - (window.location.hostname === "localhost" || - window.location.hostname === "127.0.0.1") - ) { - try { - const [prefix, queryString] = trimmed.split("?"); - if (queryString) { - const params = new URLSearchParams(queryString); - if (params.has("xs")) { - params.delete("xs"); - return `${prefix}?${params.toString()}`; - } - } - } catch (err) { - // ignore - } - } - return trimmed; -} - function normalizeTrackerList(trackers) { const normalized = []; const seen = new Set(); @@ -185,8 +161,6 @@ export class TorrentClient { // Timeout for SW operations this.TIMEOUT_DURATION = 60000; - - this.probeInFlight = new Map(); } ensureClientForProbe() { @@ -205,15 +179,9 @@ export class TorrentClient { */ async probePeers( magnetURI, - { timeoutMs = 8000, maxWebConns = 2, polls = 2, urlList = [] } = {} + { timeoutMs = 8000, maxWebConns = 2, polls = 3, urlList = [] } = {} ) { - // Debug log for CI investigation - if (typeof process !== "undefined" && process.env.NODE_ENV === "test") { - console.log(`[probePeers] magnetURI=${magnetURI?.substring(0, 20)}... urlList=${JSON.stringify(urlList)}`); - } - - const magnet = stripXsParameter(magnetURI); - + const magnet = typeof magnetURI === "string" ? magnetURI.trim() : ""; if (!magnet) { return { healthy: false, @@ -227,23 +195,11 @@ export class TorrentClient { } const trackers = normalizeTrackerList(TorrentClient.PROBE_TRACKERS); - let { magnet: augmentedMagnet, appended, hasProbeTrackers } = + const { magnet: augmentedMagnet, appended, hasProbeTrackers } = appendProbeTrackers(magnet, trackers); const hasMagnetWebSeed = magnet.includes("ws=") || magnet.includes("webSeed="); const hasExplicitWebSeed = Array.isArray(urlList) && urlList.length > 0; - - if (typeof process !== "undefined" && process.env.NODE_ENV === "test") { - console.log(`[probePeers] hasMagnetWebSeed=${hasMagnetWebSeed} hasExplicitWebSeed=${hasExplicitWebSeed}`); - } - - if (hasExplicitWebSeed) { - const wsParams = urlList - .map((url) => `ws=${encodeURIComponent(url)}`) - .join("&"); - const separator = augmentedMagnet.includes("?") ? "&" : "?"; - augmentedMagnet = `${augmentedMagnet}${separator}${wsParams}`; - } const hasWebSeed = hasMagnetWebSeed || hasExplicitWebSeed; if (!hasProbeTrackers && !hasWebSeed) { @@ -258,21 +214,14 @@ export class TorrentClient { }; } - const infoHash = infoHashFromMagnet(magnet); - const probeKey = infoHash || null; - - if (probeKey && this.probeInFlight.has(probeKey)) { - return this.probeInFlight.get(probeKey); - } - const client = this.ensureClientForProbe(); const safeTimeout = Math.max(0, normalizeNumber(timeoutMs, 8000)); - const safePolls = Math.max(0, Math.floor(normalizeNumber(polls, 2))); + const safePolls = Math.max(1, Math.floor(normalizeNumber(polls, 3))); const safeMaxWebConns = Math.max(1, Math.floor(normalizeNumber(maxWebConns, 2))); - const pollInterval = - safePolls > 0 - ? Math.max(1000, Math.floor(safeTimeout / Math.max(1, safePolls))) - : 0; + const pollInterval = Math.max( + 250, + Math.floor(safeTimeout / Math.max(1, safePolls)) + ); const startedAt = typeof performance !== "undefined" && performance?.now @@ -281,12 +230,11 @@ export class TorrentClient { emit("torrent-probe-start", { magnet: augmentedMagnet }); - const probePromise = new Promise((resolve) => { + return new Promise((resolve) => { let settled = false; let torrent = null; let timeoutId = null; let pollId = null; - let pollStartId = null; const finalize = (overrides = {}) => { if (settled) { @@ -301,10 +249,6 @@ export class TorrentClient { clearInterval(pollId); pollId = null; } - if (pollStartId) { - clearTimeout(pollStartId); - pollStartId = null; - } if (torrent) { try { torrent.destroy({ destroyStore: true }); @@ -345,9 +289,6 @@ export class TorrentClient { } torrent = client.add(augmentedMagnet, addOptions); } catch (err) { - if (typeof process !== "undefined" && process.env.NODE_ENV === "test") { - console.log(`[probePeers] client.add threw:`, err); - } finalize({ reason: "error", error: toError(err), @@ -357,114 +298,46 @@ export class TorrentClient { return; } - let webSeedCount = 0; - - const getWebSeedCount = () => { - if (torrent?.wires?.length) { - const wireCount = torrent.wires.filter( - (wire) => wire?.type === "webSeed" - ).length; - return Math.max(webSeedCount, wireCount); - } - return webSeedCount; + const settleHealthy = () => { + const peers = Math.max(1, Math.floor(normalizeNumber(torrent?.numPeers, 1))); + finalize({ healthy: true, peers, reason: "peer" }); }; - const getPeerCounts = () => { - const regularPeers = Math.max( - 0, - Math.floor(normalizeNumber(torrent?.numPeers, 0)) - ); - const webSeeds = getWebSeedCount(); - return { - regularPeers, - webSeeds, - totalPeers: regularPeers + webSeeds, - }; - }; - - const settleHealthy = (counts = getPeerCounts()) => { - const peers = Math.max(1, counts.totalPeers); - const webseedOnly = counts.regularPeers === 0 && counts.webSeeds > 0; - finalize({ - healthy: true, - peers, - reason: webseedOnly ? "webseed" : "peer", - webseedOnly, - }); - }; - - torrent.on("wire", (wire) => { - if (wire?.type === "webSeed") { - webSeedCount += 1; - } - settleHealthy(); - }); + torrent.once("wire", settleHealthy); torrent.once("error", (err) => { - const counts = getPeerCounts(); - const webseedOnly = counts.regularPeers === 0 && counts.webSeeds > 0; - const peers = Math.max(0, counts.totalPeers); + const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0))); finalize({ - healthy: webseedOnly, - reason: webseedOnly ? "webseed" : "error", + healthy: false, + reason: "error", error: toError(err), peers, - webseedOnly, + webseedOnly: peers === 0 && hasWebSeed, }); }); if (safeTimeout > 0) { timeoutId = setTimeout(() => { - const counts = getPeerCounts(); - const peers = Math.max(0, counts.totalPeers); - const webseedOnly = counts.regularPeers === 0 && counts.webSeeds > 0; - if (typeof process !== "undefined" && process.env.NODE_ENV === "test" && !webseedOnly && hasWebSeed) { - console.log(`[probePeers:timeout] logic check: peers=${peers} hasWebSeed=${hasWebSeed} webseedOnly=${webseedOnly}`); - } + const peers = Math.max(0, Math.floor(normalizeNumber(torrent?.numPeers, 0))); finalize({ - healthy: webseedOnly, + healthy: false, peers, - reason: webseedOnly ? "webseed" : "timeout", - webseedOnly, + reason: "timeout", + webseedOnly: peers === 0 && hasWebSeed, }); }, safeTimeout); } - if (safePolls > 0 && pollInterval > 0) { - pollStartId = setTimeout(() => { - if (!torrent || settled) { - return; - } - pollId = setInterval(() => { - if (!torrent || settled) { - return; - } - const counts = getPeerCounts(); - if (counts.totalPeers > 0) { - const webseedOnly = - counts.regularPeers === 0 && counts.webSeeds > 0; - finalize({ - healthy: true, - peers: counts.totalPeers, - reason: webseedOnly ? "webseed" : "peer", - webseedOnly, - }); - } - }, pollInterval); - }, pollInterval); - } - }); - - if (probeKey) { - this.probeInFlight.set(probeKey, probePromise); - probePromise.finally(() => { - if (this.probeInFlight.get(probeKey) === probePromise) { - this.probeInFlight.delete(probeKey); + pollId = setInterval(() => { + if (!torrent || settled) { + return; } - }); - } - - return probePromise; + const peers = Math.max(0, Math.floor(normalizeNumber(torrent.numPeers, 0))); + if (peers > 0) { + finalize({ healthy: true, peers, reason: "peer" }); + } + }, pollInterval); + }); } log(...args) { @@ -997,38 +870,9 @@ export class TorrentClient { */ async streamVideo(magnetURI, videoElement, opts = {}) { try { - let effectiveMagnetURI = stripXsParameter(magnetURI); - const candidateUrls = Array.isArray(opts?.urlList) - ? opts.urlList - .map((entry) => (typeof entry === "string" ? entry.trim() : "")) - .filter((entry) => /^https?:\/\//i.test(entry)) - : []; - - if (candidateUrls.length > 0) { - const wsParams = candidateUrls - .map((url) => `ws=${encodeURIComponent(url)}`) - .join("&"); - const separator = effectiveMagnetURI.includes("?") ? "&" : "?"; - effectiveMagnetURI = `${effectiveMagnetURI}${separator}${wsParams}`; - } - - if ( - effectiveMagnetURI !== - (typeof magnetURI === "string" ? magnetURI.trim() : "") - ) { - this.log( - "[WebTorrent] Stripped 'xs' parameter to prevent CORS errors on localhost.", - ); - } - - emit("torrent-stream-start", { magnet: effectiveMagnetURI }); + emit("torrent-stream-start", { magnet: magnetURI }); // 1) Make sure we have a WebTorrent client and a valid SW registration. const initResult = await this.init(); - - if (!this.client) { - throw new Error("Client destroyed during initialization"); - } - const serviceWorkerReady = !!(initResult?.serviceWorkerReady && this.swRegistration); @@ -1044,39 +888,33 @@ export class TorrentClient { } const isFirefoxBrowser = this.isFirefox(); + const candidateUrls = Array.isArray(opts?.urlList) + ? opts.urlList + .map((entry) => (typeof entry === "string" ? entry.trim() : "")) + .filter((entry) => /^https?:\/\//i.test(entry)) + : []; const chromeOptions = { strategy: "sequential" }; if (candidateUrls.length) { chromeOptions.urlList = candidateUrls; } - const addExplicitWebSeeds = (torrent) => { - if (!candidateUrls.length || typeof torrent?.addWebSeed !== "function") { - return; - } - candidateUrls.forEach((url) => { - torrent.addWebSeed(url); - }); - }; - return new Promise((resolve, reject) => { // 3) Add the torrent to the client and handle accordingly. if (isFirefoxBrowser) { this.log("Starting torrent download (Firefox path)"); this.client.add( - effectiveMagnetURI, + magnetURI, { ...chromeOptions, maxWebConns: 4 }, (torrent) => { this.log("Torrent added (Firefox path):", torrent.name); - addExplicitWebSeeds(torrent); this.handleFirefoxTorrent(torrent, videoElement, resolve, reject); } ); } else { this.log("Starting torrent download (Chrome path)"); - this.client.add(effectiveMagnetURI, chromeOptions, (torrent) => { + this.client.add(magnetURI, chromeOptions, (torrent) => { this.log("Torrent added (Chrome path):", torrent.name); - addExplicitWebSeeds(torrent); this.handleChromeTorrent(torrent, videoElement, resolve, reject); }); } diff --git a/tests/webtorrent-regression.test.mjs b/tests/webtorrent-regression.test.mjs index ea79476f..7143aabf 100644 --- a/tests/webtorrent-regression.test.mjs +++ b/tests/webtorrent-regression.test.mjs @@ -84,13 +84,14 @@ describe("WebTorrent Regression Tests", () => { urlList: [webSeedUrl] }); - if (!result.webseedOnly) { + if (!result.healthy) { console.log("Failed result:", result); } - assert.strictEqual(result.webseedOnly, true, "Should detect webseed only scenario"); + // The simplified implementation treats webseeds as peers assert.strictEqual(result.healthy, true, "Should be healthy due to webseed"); - assert.strictEqual(result.reason, "webseed", "Reason should be 'webseed'"); + assert.strictEqual(result.reason, "peer", "Reason should be 'peer'"); + assert.ok(result.peers >= 1, "Should report at least 1 peer"); }); it("probePeers should report unhealthy if no webseed and 0 peers", async () => {