From f06b2c7b1dbc8ad63b64e2922fdf01f11545cb8a Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 18:13:37 +0000 Subject: [PATCH] feat: persist webtorrent probes and tracker health checks to localstorage - Add storage helpers for torrent probes and tracker health in `js/utils/storage.js` - Update `js/gridHealth.js` to read/write torrent probes from localStorage - Update `js/healthService.js` to read/write tracker health from localStorage - Ensure TTLs (5m for probes, 2m for trackers) are respected across sessions - Add verification test `tests/verify-storage-persistence.mjs` (removed before completion) --- js/gridHealth.js | 26 +++++++++- js/healthService.js | 22 ++++++++- js/utils/storage.js | 114 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 4 deletions(-) diff --git a/js/gridHealth.js b/js/gridHealth.js index eac90b61..f565ebb9 100644 --- a/js/gridHealth.js +++ b/js/gridHealth.js @@ -3,6 +3,11 @@ import { infoHashFromMagnet } from "./magnets.js"; import { updateVideoCardSourceVisibility } from "./utils/cardSourceVisibility.js"; import { TorrentClient, torrentClient } from "./webtorrent.js"; import { userLogger } from "./utils/logger.js"; +import { + readTorrentProbeFromStorage, + removeTorrentProbeFromStorage, + writeTorrentProbeToStorage, +} from "./utils/storage.js"; const badgeUpdateListeners = new Set(); @@ -33,14 +38,29 @@ function getCacheEntry(key) { if (!key) { return null; } - const entry = probeCache.get(key); + let entry = probeCache.get(key); + + if (!entry) { + // Try storage if memory miss + entry = readTorrentProbeFromStorage(key); + } + if (!entry) { return null; } + + // Verify freshness if (now() - entry.ts > PROBE_CACHE_TTL_MS) { probeCache.delete(key); + removeTorrentProbeFromStorage(key); return null; } + + // Populate memory if it came from storage + if (!probeCache.has(key)) { + probeCache.set(key, entry); + } + return entry.value; } @@ -48,7 +68,9 @@ function setCacheEntry(key, value) { if (!key) { return; } - probeCache.set(key, { ts: now(), value }); + const entry = { ts: now(), value }; + probeCache.set(key, entry); + writeTorrentProbeToStorage(key, entry); } function formatTime(ts) { diff --git a/js/healthService.js b/js/healthService.js index 8e8a0f4b..bab24b41 100644 --- a/js/healthService.js +++ b/js/healthService.js @@ -3,6 +3,11 @@ import { trackerPing } from "./trackerPing.js"; import { infoHashFromMagnet } from "./magnets.js"; import { HEALTH_TTL_MS, CONCURRENCY } from "./trackerConfig.js"; import { userLogger } from "./utils/logger.js"; +import { + readTrackerHealthFromStorage, + removeTrackerHealthFromStorage, + writeTrackerHealthToStorage, +} from "./utils/storage.js"; const queue = new PQueue({ concurrency: CONCURRENCY }); const cache = new Map(); @@ -22,14 +27,25 @@ export function getHealthCached(infoHash) { if (!infoHash) { return null; } - const entry = cache.get(infoHash); + let entry = cache.get(infoHash); + + if (!entry) { + entry = readTrackerHealthFromStorage(infoHash); + } + if (!entry) { return null; } if (Date.now() - entry.ts > HEALTH_TTL_MS) { cache.delete(infoHash); + removeTrackerHealthFromStorage(infoHash); return null; } + + if (!cache.has(infoHash)) { + cache.set(infoHash, entry); + } + return entry.value; } @@ -37,7 +53,9 @@ export function setHealthCache(infoHash, value) { if (!infoHash) { return; } - cache.set(infoHash, { ts: Date.now(), value }); + const entry = { ts: Date.now(), value }; + cache.set(infoHash, entry); + writeTrackerHealthToStorage(infoHash, entry); } export function queueHealthCheck(magnet, onResult) { diff --git a/js/utils/storage.js b/js/utils/storage.js index 0cc3ec56..479d18d1 100644 --- a/js/utils/storage.js +++ b/js/utils/storage.js @@ -2,11 +2,21 @@ import { userLogger } from "./logger.js"; // js/utils/storage.js const URL_HEALTH_STORAGE_PREFIX = "bitvid:urlHealth:"; +const TORRENT_PROBE_STORAGE_PREFIX = "bitvid:torrentProbe:"; +const TRACKER_HEALTH_STORAGE_PREFIX = "bitvid:trackerHealth:"; export function getUrlHealthStorageKey(eventId) { return `${URL_HEALTH_STORAGE_PREFIX}${eventId}`; } +export function getTorrentProbeStorageKey(infoHash) { + return `${TORRENT_PROBE_STORAGE_PREFIX}${infoHash}`; +} + +export function getTrackerHealthStorageKey(infoHash) { + return `${TRACKER_HEALTH_STORAGE_PREFIX}${infoHash}`; +} + export function readUrlHealthFromStorage(eventId) { if (!eventId || typeof localStorage === "undefined") { return null; @@ -55,3 +65,107 @@ export function removeUrlHealthFromStorage(eventId) { userLogger.warn(`Failed to remove URL health for ${eventId}:`, err); } } + +export function readTorrentProbeFromStorage(infoHash) { + if (!infoHash || typeof localStorage === "undefined") { + return null; + } + + try { + const raw = localStorage.getItem(getTorrentProbeStorageKey(infoHash)); + if (!raw) { + return null; + } + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === "object") { + return parsed; + } + } catch (err) { + userLogger.warn( + `Failed to parse stored torrent probe for ${infoHash}:`, + err + ); + } + + removeTorrentProbeFromStorage(infoHash); + return null; +} + +export function writeTorrentProbeToStorage(infoHash, entry) { + if (!infoHash || typeof localStorage === "undefined") { + return; + } + + try { + localStorage.setItem( + getTorrentProbeStorageKey(infoHash), + JSON.stringify(entry) + ); + } catch (err) { + userLogger.warn(`Failed to persist torrent probe for ${infoHash}:`, err); + } +} + +export function removeTorrentProbeFromStorage(infoHash) { + if (!infoHash || typeof localStorage === "undefined") { + return; + } + + try { + localStorage.removeItem(getTorrentProbeStorageKey(infoHash)); + } catch (err) { + userLogger.warn(`Failed to remove torrent probe for ${infoHash}:`, err); + } +} + +export function readTrackerHealthFromStorage(infoHash) { + if (!infoHash || typeof localStorage === "undefined") { + return null; + } + + try { + const raw = localStorage.getItem(getTrackerHealthStorageKey(infoHash)); + if (!raw) { + return null; + } + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === "object") { + return parsed; + } + } catch (err) { + userLogger.warn( + `Failed to parse stored tracker health for ${infoHash}:`, + err + ); + } + + removeTrackerHealthFromStorage(infoHash); + return null; +} + +export function writeTrackerHealthToStorage(infoHash, entry) { + if (!infoHash || typeof localStorage === "undefined") { + return; + } + + try { + localStorage.setItem( + getTrackerHealthStorageKey(infoHash), + JSON.stringify(entry) + ); + } catch (err) { + userLogger.warn(`Failed to persist tracker health for ${infoHash}:`, err); + } +} + +export function removeTrackerHealthFromStorage(infoHash) { + if (!infoHash || typeof localStorage === "undefined") { + return; + } + + try { + localStorage.removeItem(getTrackerHealthStorageKey(infoHash)); + } catch (err) { + userLogger.warn(`Failed to remove tracker health for ${infoHash}:`, err); + } +}