Cache health badge state to avoid re-flashing

This commit is contained in:
thePR0M3TH3AN committed 2025-10-09 20:31:40 -04:00
1 parent 96bbeef040
commit e2935c23fd
5 files changed
+536 -21

No files matched your search

+70 -1
View File
@@ -162,6 +162,15 @@ class Application {
? helpers.mediaLoader
: mediaLoaderFactory();
this.loadedThumbnails = new Map();
this.urlHealthSnapshots = new Map();
this.streamHealthSnapshots = new Map();
this.boundStreamHealthBadgeHandler = (detail) =>
this.handleStreamHealthBadgeUpdate(detail);
this.attachHealthBadgesWithCache = (container) => {
attachHealthBadges(container, {
onUpdate: this.boundStreamHealthBadgeHandler,
});
};
this.relayManager = relayManager;
this.activeIntervals = [];
this.watchHistoryTelemetry = null;
@@ -787,7 +796,11 @@ class Application {
const videoListViewConfig = {
document,
mediaLoader: this.mediaLoader,
badgeHelpers: { attachHealthBadges, attachUrlHealthBadges },
badgeHelpers: {
attachHealthBadges: this.attachHealthBadgesWithCache,
attachUrlHealthBadges: (container, onCheck) =>
attachUrlHealthBadges(container, onCheck),
},
formatters: {
formatTimeAgo: (timestamp) => this.formatTimeAgo(timestamp),
formatViewCountLabel: (total) => this.formatViewCountLabel(total),
@@ -805,6 +818,8 @@ class Application {
state: {
loadedThumbnails: this.loadedThumbnails,
videosMap: this.videosMap,
urlHealthByVideoId: this.urlHealthSnapshots,
streamHealthByVideoId: this.streamHealthSnapshots,
},
utils: {
dedupeVideos: (videos) => this.dedupeVideosByRoot(videos),
@@ -4104,6 +4119,20 @@ class Application {
"text-gray-300"
);
}
if (
videoId &&
this.videoListView &&
typeof this.videoListView.cacheUrlHealth === "function"
) {
this.videoListView.cacheUrlHealth(videoId, {
status,
message,
lastCheckedAt: Number.isFinite(state?.lastCheckedAt)
? state.lastCheckedAt
: undefined,
});
}
}
handleUrlHealthBadge({ video, url, badgeEl }) {
@@ -4201,6 +4230,46 @@ class Application {
});
}
handleStreamHealthBadgeUpdate(detail) {
if (!detail || typeof detail !== "object") {
return;
}
const card = detail.card;
if (!(card instanceof HTMLElement)) {
return;
}
let videoId =
(card.dataset && card.dataset.videoId) ||
(typeof card.getAttribute === "function" ? card.getAttribute("data-video-id") : "") ||
"";
if (!videoId && typeof card.querySelector === "function") {
const fallback = card.querySelector("[data-video-id]");
if (fallback instanceof HTMLElement && fallback.dataset.videoId) {
videoId = fallback.dataset.videoId;
}
}
if (!videoId) {
return;
}
if (this.videoListView && typeof this.videoListView.cacheStreamHealth === "function") {
this.videoListView.cacheStreamHealth(videoId, {
state: detail.state,
peers: detail.peers,
reason: detail.reason,
checkedAt: detail.checkedAt,
text: detail.text,
tooltip: detail.tooltip,
role: detail.role,
ariaLive: detail.ariaLive,
});
}
}
async renderVideoList(payload) {
if (!this.videoListView) {
return;
+39 -2
View File
@@ -2,6 +2,25 @@ import { createCardObserver } from "./dom/cardObserver.js";
import { infoHashFromMagnet } from "./magnets.js";
import { TorrentClient, torrentClient } from "./webtorrent.js";
const badgeUpdateListeners = new Set();
function notifyBadgeUpdate(payload) {
if (!payload) {
return;
}
badgeUpdateListeners.forEach((listener) => {
if (typeof listener !== "function") {
return;
}
try {
listener(payload);
} catch (err) {
console.warn("[gridHealth] badge listener failed", err);
}
});
}
const ROOT_MARGIN = "200px 0px";
function now() {
@@ -479,7 +498,8 @@ function setBadge(card, state, details) {
});
badge.setAttribute("aria-label", tooltip);
badge.setAttribute("title", tooltip);
badge.setAttribute("aria-live", entry.role === "alert" ? "assertive" : "polite");
const ariaLive = entry.role === "alert" ? "assertive" : "polite";
badge.setAttribute("aria-live", ariaLive);
badge.setAttribute("role", entry.role);
badge.dataset.streamHealthState = normalizedState;
if (hasPeerCount) {
@@ -487,6 +507,20 @@ function setBadge(card, state, details) {
} else if (badge.dataset.streamHealthPeers) {
delete badge.dataset.streamHealthPeers;
}
const payload = {
card,
state: normalizedState,
peers: hasPeerCount ? peersValue : null,
reason: details && typeof details.reason === "string" ? details.reason : null,
checkedAt:
details && Number.isFinite(details.checkedAt) ? Number(details.checkedAt) : null,
text: badge.textContent,
tooltip,
role: entry.role,
ariaLive,
};
notifyBadgeUpdate(payload);
}
function handleCardVisible({ card, pendingByCard, priority = 0 }) {
@@ -597,10 +631,13 @@ const gridCardObserver = createCardObserver({
},
});
export function attachHealthBadges(container) {
export function attachHealthBadges(container, options = {}) {
if (!(container instanceof HTMLElement)) {
return;
}
if (options && typeof options.onUpdate === "function") {
badgeUpdateListeners.add(options.onUpdate);
}
gridCardObserver.observe(container);
}
+16
View File
@@ -518,6 +518,20 @@ class SubscriptionsManager {
? baseView.state.videosMap
: new Map();
const urlHealthCache =
app?.urlHealthSnapshots instanceof Map
? app.urlHealthSnapshots
: baseView?.state?.urlHealthByVideoId instanceof Map
? baseView.state.urlHealthByVideoId
: new Map();
const streamHealthCache =
app?.streamHealthSnapshots instanceof Map
? app.streamHealthSnapshots
: baseView?.state?.streamHealthByVideoId instanceof Map
? baseView.state.streamHealthByVideoId
: new Map();
const listViewConfig = {
document: doc,
container,
@@ -538,6 +552,8 @@ class SubscriptionsManager {
state: {
loadedThumbnails,
videosMap,
urlHealthByVideoId: urlHealthCache,
streamHealthByVideoId: streamHealthCache,
},
utils: {
dedupeVideos: (videos) => (Array.isArray(videos) ? [...videos] : []),
+304 -17
View File
@@ -56,6 +56,12 @@ export class VideoCard {
this.state = {
loadedThumbnails:
state.loadedThumbnails instanceof Map ? state.loadedThumbnails : null,
urlHealthByVideoId:
state.urlHealthByVideoId instanceof Map ? state.urlHealthByVideoId : null,
streamHealthByVideoId:
state.streamHealthByVideoId instanceof Map
? state.streamHealthByVideoId
: null,
};
this.ensureGlobalMoreMenuHandlers =
typeof ensureGlobalMoreMenuHandlers === "function"
@@ -210,6 +216,10 @@ export class VideoCard {
this.root = root;
if (this.video?.id) {
root.dataset.videoId = this.video.id;
}
this.applyPointerDataset();
this.applyOwnerDataset();
this.applySourceDatasets();
@@ -887,17 +897,13 @@ export class VideoCard {
"px-2",
"py-1",
"rounded",
"inline-flex",
"items-center",
"gap-1",
"bg-gray-800",
"text-gray-300",
"transition-colors",
"duration-200",
],
textContent: "Checking hosted URL…",
});
badge.dataset.urlHealthState = "checking";
badge.setAttribute("aria-live", "polite");
badge.setAttribute("role", "status");
this.applyUrlBadgeVisualState(badge, this.getCachedUrlHealthEntry());
this.urlHealthBadgeEl = badge;
pieces.push(badge);
}
@@ -911,19 +917,13 @@ export class VideoCard {
"px-2",
"py-1",
"rounded",
"inline-flex",
"items-center",
"gap-1",
"bg-gray-800",
"text-gray-300",
"transition-colors",
"duration-200",
],
textContent: "⏳ Torrent",
});
badge.dataset.streamHealthState = "checking";
badge.setAttribute("aria-live", "polite");
badge.setAttribute("role", "status");
this.applyStreamBadgeVisualState(badge, this.getCachedStreamHealthEntry());
this.torrentHealthBadgeEl = badge;
pieces.push(badge);
}
@@ -939,6 +939,269 @@ export class VideoCard {
return container;
}
getCachedUrlHealthEntry() {
if (!this.video?.id || !(this.state.urlHealthByVideoId instanceof Map)) {
return null;
}
const entry = this.state.urlHealthByVideoId.get(this.video.id);
if (!entry || typeof entry !== "object") {
return null;
}
const normalized = {};
if (typeof entry.status === "string" && entry.status) {
normalized.status = entry.status;
}
if (typeof entry.message === "string" && entry.message) {
normalized.message = entry.message;
}
if (Number.isFinite(entry.lastCheckedAt)) {
normalized.lastCheckedAt = Math.floor(entry.lastCheckedAt);
}
return Object.keys(normalized).length ? normalized : null;
}
getCachedStreamHealthEntry() {
if (!this.video?.id || !(this.state.streamHealthByVideoId instanceof Map)) {
return null;
}
const entry = this.state.streamHealthByVideoId.get(this.video.id);
if (!entry || typeof entry !== "object") {
return null;
}
const normalized = {};
if (typeof entry.state === "string" && entry.state) {
normalized.state = entry.state;
}
if (Number.isFinite(entry.peers)) {
normalized.peers = Math.max(0, Number(entry.peers));
}
if (typeof entry.reason === "string" && entry.reason) {
normalized.reason = entry.reason;
}
if (Number.isFinite(entry.checkedAt)) {
normalized.checkedAt = Math.floor(entry.checkedAt);
}
if (typeof entry.text === "string" && entry.text) {
normalized.text = entry.text;
}
if (typeof entry.tooltip === "string" && entry.tooltip) {
normalized.tooltip = entry.tooltip;
}
if (entry.role === "alert" || entry.role === "status") {
normalized.role = entry.role;
}
if (entry.ariaLive === "assertive" || entry.ariaLive === "polite") {
normalized.ariaLive = entry.ariaLive;
}
return Object.keys(normalized).length ? normalized : null;
}
formatTorrentCheckedTime(timestamp) {
if (!Number.isFinite(timestamp)) {
return "";
}
try {
return new Date(timestamp).toLocaleTimeString([], { hour12: false });
} catch (error) {
try {
return new Date(timestamp).toLocaleTimeString();
} catch (err) {
return "";
}
}
}
buildTorrentTooltip({ peers = null, checkedAt = null, reason = null } = {}) {
const parts = [];
if (Number.isFinite(peers)) {
parts.push(`Peers: ${Math.max(0, Number(peers))}`);
}
if (Number.isFinite(checkedAt)) {
const formatted = this.formatTorrentCheckedTime(checkedAt);
if (formatted) {
parts.push(`Checked ${formatted}`);
}
}
if (typeof reason === "string" && reason && reason !== "peer") {
let normalizedReason;
if (reason === "timeout") {
normalizedReason = "Timed out";
} else if (reason === "no-trackers") {
normalizedReason = "No WSS trackers";
} else if (reason === "invalid") {
normalizedReason = "Invalid magnet";
} else {
normalizedReason = reason.charAt(0).toUpperCase() + reason.slice(1);
}
parts.push(normalizedReason);
}
if (!parts.length) {
return "WebTorrent status unknown";
}
return `WebTorrent • ${parts.join(" • ")}`;
}
applyUrlBadgeVisualState(badge, entry) {
if (!(badge instanceof HTMLElement)) {
return;
}
const status =
typeof entry?.status === "string" && entry.status ? entry.status : "checking";
const message =
typeof entry?.message === "string" && entry.message
? entry.message
: "Checking hosted URL…";
const hadMargin = badge.classList.contains("mt-3");
const baseClasses = [
"url-health-badge",
"text-xs",
"font-semibold",
"px-2",
"py-1",
"rounded",
"transition-colors",
"duration-200",
];
if (hadMargin) {
baseClasses.unshift("mt-3");
}
badge.className = baseClasses.join(" ");
const common = ["inline-flex", "items-center", "gap-1"];
const addClasses = (classes) => {
classes.forEach((cls) => badge.classList.add(cls));
};
if (status === "healthy") {
addClasses([...common, "bg-green-900", "text-green-200"]);
} else if (status === "offline") {
addClasses([...common, "bg-red-900", "text-red-200"]);
} else if (status === "unknown" || status === "timeout") {
addClasses([...common, "bg-amber-900", "text-amber-200"]);
} else {
addClasses([...common, "bg-gray-800", "text-gray-300"]);
}
badge.dataset.urlHealthState = status;
badge.textContent = message;
badge.setAttribute("aria-live", "polite");
badge.setAttribute("role", status === "offline" ? "alert" : "status");
}
applyStreamBadgeVisualState(badge, entry) {
if (!(badge instanceof HTMLElement)) {
return;
}
const state =
typeof entry?.state === "string" && entry.state ? entry.state : "checking";
const peersValue = Number.isFinite(entry?.peers)
? Math.max(0, Number(entry.peers))
: null;
const reason = typeof entry?.reason === "string" && entry.reason ? entry.reason : null;
const text = typeof entry?.text === "string" && entry.text ? entry.text : null;
const tooltip =
typeof entry?.tooltip === "string" && entry.tooltip ? entry.tooltip : null;
const role = entry?.role === "alert" || entry?.role === "status" ? entry.role : null;
const ariaLive =
entry?.ariaLive === "assertive" || entry?.ariaLive === "polite"
? entry.ariaLive
: role === "alert"
? "assertive"
: "polite";
const hadMargin = badge.classList.contains("mt-3");
const baseClasses = [
"torrent-health-badge",
"text-xs",
"font-semibold",
"px-2",
"py-1",
"rounded",
"transition-colors",
"duration-200",
];
if (hadMargin) {
baseClasses.unshift("mt-3");
}
badge.className = baseClasses.join(" ");
const common = ["inline-flex", "items-center", "gap-1"];
const addClasses = (classes) => {
classes.forEach((cls) => badge.classList.add(cls));
};
if (state === "healthy") {
addClasses([...common, "bg-green-900", "text-green-200"]);
} else if (state === "unhealthy") {
addClasses([...common, "bg-red-900", "text-red-200"]);
} else {
addClasses([...common, "bg-gray-800", "text-gray-300"]);
}
const map = {
healthy: {
icon: "🟢",
aria: "WebTorrent peers available",
},
unhealthy: {
icon: "🔴",
aria: "WebTorrent peers unavailable",
},
checking: {
icon: "⏳",
aria: "Checking WebTorrent peers",
},
unknown: {
icon: "⚪",
aria: "WebTorrent status unknown",
},
};
const descriptor = map[state] || map.unknown;
const peersText = state === "healthy" && peersValue > 0 ? ` (${peersValue})` : "";
const iconPrefix = descriptor.icon ? `${descriptor.icon} ` : "";
const computedText = `${iconPrefix}WebTorrent${peersText}`;
badge.textContent = text || computedText;
const tooltipValue =
tooltip ||
(state === "checking" || state === "unknown"
? descriptor.aria
: this.buildTorrentTooltip({
peers: peersValue,
checkedAt: Number.isFinite(entry?.checkedAt) ? entry.checkedAt : null,
reason,
}));
badge.setAttribute("aria-label", tooltipValue);
badge.setAttribute("title", tooltipValue);
badge.setAttribute("aria-live", ariaLive);
badge.setAttribute("role", role || (state === "unhealthy" ? "alert" : "status"));
badge.dataset.streamHealthState = state;
if (reason) {
badge.dataset.streamHealthReason = reason;
} else if (badge.dataset.streamHealthReason) {
delete badge.dataset.streamHealthReason;
}
if (Number.isFinite(peersValue) && peersValue !== null) {
badge.dataset.streamHealthPeers = String(peersValue);
} else if (badge.dataset.streamHealthPeers) {
delete badge.dataset.streamHealthPeers;
}
}
buildDiscussionCount() {
if (this.video.enableComments === false) {
return null;
@@ -1033,8 +1296,13 @@ export class VideoCard {
return;
}
const cachedUrlHealth = this.getCachedUrlHealthEntry();
if (this.playbackUrl) {
this.root.dataset.urlHealthState = "checking";
const status =
typeof cachedUrlHealth?.status === "string" && cachedUrlHealth.status
? cachedUrlHealth.status
: "checking";
this.root.dataset.urlHealthState = status;
delete this.root.dataset.urlHealthReason;
this.root.dataset.urlHealthEventId = this.video.id || "";
this.root.dataset.urlHealthUrl = encodeURIComponent(this.playbackUrl);
@@ -1045,14 +1313,33 @@ export class VideoCard {
delete this.root.dataset.urlHealthUrl;
}
const cachedStreamHealth = this.getCachedStreamHealthEntry();
if (this.magnetProvided && this.magnetSupported) {
this.root.dataset.streamHealthState = "checking";
delete this.root.dataset.streamHealthReason;
const state =
typeof cachedStreamHealth?.state === "string" && cachedStreamHealth.state
? cachedStreamHealth.state
: "checking";
this.root.dataset.streamHealthState = state;
if (cachedStreamHealth?.reason) {
this.root.dataset.streamHealthReason = cachedStreamHealth.reason;
} else if (this.root.dataset.streamHealthReason) {
delete this.root.dataset.streamHealthReason;
}
if (Number.isFinite(cachedStreamHealth?.peers)) {
this.root.dataset.streamHealthPeers = String(
Math.max(0, Number(cachedStreamHealth.peers))
);
} else if (this.root.dataset.streamHealthPeers) {
delete this.root.dataset.streamHealthPeers;
}
} else {
this.root.dataset.streamHealthState = "unhealthy";
this.root.dataset.streamHealthReason = this.magnetProvided
? "unsupported"
: "missing-source";
if (this.root.dataset.streamHealthPeers) {
delete this.root.dataset.streamHealthPeers;
}
}
if (this.magnetProvided) {
+107 -1
View File
@@ -54,6 +54,14 @@ export class VideoListView {
loadedThumbnails:
state.loadedThumbnails instanceof Map ? state.loadedThumbnails : new Map(),
videosMap: state.videosMap instanceof Map ? state.videosMap : new Map(),
urlHealthByVideoId:
state.urlHealthByVideoId instanceof Map
? state.urlHealthByVideoId
: new Map(),
streamHealthByVideoId:
state.streamHealthByVideoId instanceof Map
? state.streamHealthByVideoId
: new Map(),
feedMetadata:
state.feedMetadata && typeof state.feedMetadata === "object"
? { ...state.feedMetadata }
@@ -268,6 +276,7 @@ export class VideoListView {
this.syncViewCountSubscriptions(displayVideos);
this.cleanupThumbnailCache(displayVideos);
this.cleanupHealthCache(displayVideos);
if (!displayVideos.length) {
this.renderedVideoIds.clear();
@@ -383,7 +392,11 @@ export class VideoListView {
fallbackThumbnailSrc: this.assets.fallbackThumbnailSrc,
unsupportedBtihMessage: this.assets.unsupportedBtihMessage,
},
state: { loadedThumbnails: this.state.loadedThumbnails },
state: {
loadedThumbnails: this.state.loadedThumbnails,
urlHealthByVideoId: this.state.urlHealthByVideoId,
streamHealthByVideoId: this.state.streamHealthByVideoId,
},
ensureGlobalMoreMenuHandlers: () =>
this.utils.ensureGlobalMoreMenuHandlers(),
onRequestCloseAllMenus: () => this.closeAllMenus(),
@@ -517,6 +530,99 @@ export class VideoListView {
return displayVideos;
}
cacheUrlHealth(videoId, entry) {
if (!videoId || !(this.state.urlHealthByVideoId instanceof Map)) {
return;
}
if (!entry || typeof entry !== "object") {
this.state.urlHealthByVideoId.delete(videoId);
return;
}
const normalized = {
status:
typeof entry.status === "string" && entry.status ? entry.status : "checking",
message:
typeof entry.message === "string" && entry.message
? entry.message
: "Checking hosted URL…",
};
if (Number.isFinite(entry.lastCheckedAt)) {
normalized.lastCheckedAt = Math.floor(entry.lastCheckedAt);
}
this.state.urlHealthByVideoId.set(videoId, normalized);
}
cacheStreamHealth(videoId, entry) {
if (!videoId || !(this.state.streamHealthByVideoId instanceof Map)) {
return;
}
if (!entry || typeof entry !== "object") {
this.state.streamHealthByVideoId.delete(videoId);
return;
}
const normalized = {
state:
typeof entry.state === "string" && entry.state ? entry.state : "unknown",
};
if (Number.isFinite(entry.peers)) {
normalized.peers = Math.max(0, Number(entry.peers));
}
if (typeof entry.reason === "string" && entry.reason) {
normalized.reason = entry.reason;
}
if (Number.isFinite(entry.checkedAt)) {
normalized.checkedAt = Math.floor(entry.checkedAt);
}
if (typeof entry.text === "string" && entry.text) {
normalized.text = entry.text;
}
if (typeof entry.tooltip === "string" && entry.tooltip) {
normalized.tooltip = entry.tooltip;
}
if (entry.role === "alert" || entry.role === "status") {
normalized.role = entry.role;
}
if (entry.ariaLive === "assertive" || entry.ariaLive === "polite") {
normalized.ariaLive = entry.ariaLive;
}
this.state.streamHealthByVideoId.set(videoId, normalized);
}
cleanupHealthCache(videos) {
const activeIds = new Set();
if (Array.isArray(videos)) {
videos.forEach((video) => {
if (video && typeof video.id === "string" && video.id) {
activeIds.add(video.id);
}
});
}
if (this.state.urlHealthByVideoId instanceof Map) {
Array.from(this.state.urlHealthByVideoId.keys()).forEach((videoId) => {
if (!activeIds.has(videoId)) {
this.state.urlHealthByVideoId.delete(videoId);
}
});
}
if (this.state.streamHealthByVideoId instanceof Map) {
Array.from(this.state.streamHealthByVideoId.keys()).forEach((videoId) => {
if (!activeIds.has(videoId)) {
this.state.streamHealthByVideoId.delete(videoId);
}
});
}
}
updateVideo(videoId, patch) {
if (!videoId) {
return null;