Files
bitvid/js/urlHealthObserver.js
T
thePR0M3TH3ANandClaude Opus 4.8 ccbe8d9bb2 feat(liveness): config-driven card visibility policy + probe prefetch (#5)
Makes the card liveness-visibility behavior switchable so it can be A/B
feel-tested, and probes ahead of the scroll for a snappier result.

- CARD_LIVENESS_POLICY (config/instance-config.js): "show-pending" (default,
  unchanged) | "hide-foreign" (foreign/ingested cards hidden until a source
  verifies; native cards stay show-pending) | "hide-all". Owner's own cards are
  always visible. Flip live for testing without a rebuild:
  window.__BITVID_CARD_LIVENESS_POLICY__ = "hide-foreign" then refresh/scroll.
  Implemented via a data-foreign provenance flag on the card +
  getCardLivenessPolicy() in cardSourceVisibility.js.
- LIVENESS_PROBE_PREFETCH_MARGIN (default 600px): both liveness probes were
  already viewport-gated (IntersectionObserver, on-screen-first, WebTorrent
  priority-queued + concurrency-capped); this expands their rootMargin so cards
  just below the fold verify before the user reaches them.
- Tests: three policy scenarios in tests/video-card-source-visibility.test.mjs;
  also un-broke 3 pre-existing VideoCard tests by adding the missing
  IntersectionObserver JSDOM stub.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-25 09:13:52 -04:00

72 lines
2.0 KiB
JavaScript

import { createCardObserver } from "./dom/cardObserver.js";
import { safeDecodeURIComponent } from "./utils/safeDecode.js";
import { userLogger } from "./utils/logger.js";
import { getLivenessProbePrefetchMargin } from "./constants.js";
// Prefetch margin lets cards just below the fold start probing before they scroll
// into view, so they're verified by the time the user reaches them.
const ROOT_MARGIN = getLivenessProbePrefetchMargin();
const THRESHOLD = 0.25;
const urlCardObserver = createCardObserver({
rootMargin: ROOT_MARGIN,
threshold: THRESHOLD,
isCardVisible: (entry) => {
if (!entry) {
return false;
}
const isIntersecting = Boolean(entry.isIntersecting);
const ratio = typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0;
return isIntersecting && ratio > 0;
},
createState: () => ({ onCheck: null }),
onCardVisible: ({ card, state }) => {
if (!state || typeof state.onCheck !== "function") {
return;
}
const badgeEl = card.querySelector("[data-url-health-state]");
if (!(badgeEl instanceof HTMLElement)) {
return;
}
const encodedUrl =
card.dataset.urlHealthUrl || badgeEl.dataset.urlHealthUrl || "";
const eventId =
card.dataset.urlHealthEventId || badgeEl.dataset.urlHealthEventId || "";
if (!encodedUrl || !eventId) {
return;
}
const currentState =
card.dataset.urlHealthState || badgeEl.dataset.urlHealthState;
if (currentState && currentState !== "checking") {
return;
}
const url = safeDecodeURIComponent(encodedUrl);
if (!url) {
return;
}
try {
state.onCheck({ card, badgeEl, url, eventId });
} catch (err) {
userLogger.warn("[urlHealthObserver] onCheck handler failed", err);
}
},
});
export function attachUrlHealthBadges(container, onCheck) {
if (!(container instanceof HTMLElement)) {
return;
}
const state = urlCardObserver.observe(container);
if (state && typeof onCheck === "function") {
state.onCheck = onCheck;
}
}