fix(liveness): eager-probe hide-until-verified cards (fixes vanished foreign feed)

Setting CARD_LIVENESS_POLICY=hide-foreign made all foreign/NIP-71 content
disappear: a hide-until-verified card starts display:none, which has no layout
box, so the IntersectionObserver never fires for it — its liveness probe never
runs, it never gets a healthy verdict, and it stays hidden forever (deadlock).

Fix: cardNeedsEagerLivenessProbe() identifies cards governed by a
hide-until-verified policy (foreign under hide-foreign; all non-owner under
hide-all). Both health observers now kick the probe for those cards from
onCardRegister — which fires for every card regardless of visibility — instead of
waiting for an intersection that a hidden card can never produce. The URL probe is
deferred one microtask so attachUrlHealthBadges has wired state.onCheck after
observe(). Probes stay concurrency-capped + priority-ordered; show-pending is
unchanged (no eager probing).

Tests: cardNeedsEagerLivenessProbe predicate coverage in
tests/video-card-source-visibility.test.mjs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-25 09:33:41 -04:00
1 parent a3b66f8841
commit 129d877d38
5 files changed
+131 -39

No files matched your search

+49 -34
View File
@@ -2,6 +2,7 @@ import { createCardObserver } from "./dom/cardObserver.js";
import { safeDecodeURIComponent } from "./utils/safeDecode.js";
import { userLogger } from "./utils/logger.js";
import { getLivenessProbePrefetchMargin } from "./constants.js";
import { cardNeedsEagerLivenessProbe } from "./utils/cardSourceVisibility.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.
@@ -20,44 +21,58 @@ const urlCardObserver = createCardObserver({
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);
// Hide-until-verified cards start `display:none`, so the IntersectionObserver
// never fires for them — kick the URL probe eagerly on register so they can be
// verified and revealed (the alternative is they stay hidden forever).
onCardRegister: ({ card, state }) => {
if (cardNeedsEagerLivenessProbe(card)) {
// attachUrlHealthBadges sets state.onCheck right AFTER observe() returns, so
// defer one microtask to let that wiring land before we probe.
queueMicrotask(() => triggerUrlCheck(card, state));
}
},
onCardVisible: ({ card, state }) => {
triggerUrlCheck(card, state);
},
});
function triggerUrlCheck(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;