mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
1 parent
a3b66f8841
commit
129d877d38
5 files changed
+131
-39
No files matched your search
+20
-2
@@ -1,6 +1,9 @@
|
||||
import { createCardObserver } from "./dom/cardObserver.js";
|
||||
import { infoHashFromMagnet } from "./magnets.js";
|
||||
import { updateVideoCardSourceVisibility } from "./utils/cardSourceVisibility.js";
|
||||
import {
|
||||
updateVideoCardSourceVisibility,
|
||||
cardNeedsEagerLivenessProbe,
|
||||
} from "./utils/cardSourceVisibility.js";
|
||||
import { TorrentClient, torrentClient } from "./webtorrent.js";
|
||||
import { userLogger } from "./utils/logger.js";
|
||||
import {
|
||||
@@ -532,9 +535,24 @@ const gridCardObserver = createCardObserver({
|
||||
const ratio = typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0;
|
||||
return isIntersecting && ratio > 0;
|
||||
},
|
||||
onCardRegister: ({ card }) => {
|
||||
onCardRegister: ({ card, state }) => {
|
||||
if (!card.dataset.magnet) {
|
||||
setBadge(card, "unhealthy", { reason: "missing-source" });
|
||||
return;
|
||||
}
|
||||
// Hide-until-verified cards start hidden, so the viewport observer can never
|
||||
// fire for them — probe eagerly on register (still concurrency-capped) so
|
||||
// they can be verified and revealed instead of staying hidden forever.
|
||||
if (
|
||||
cardNeedsEagerLivenessProbe(card) &&
|
||||
state &&
|
||||
state.pendingByCard instanceof WeakMap
|
||||
) {
|
||||
handleCardVisible({
|
||||
card,
|
||||
pendingByCard: state.pendingByCard,
|
||||
priority: PRIORITY_BASELINE,
|
||||
});
|
||||
}
|
||||
},
|
||||
onCardVisible: ({ card, meta, state }) => {
|
||||
|
||||
+49
-34
@@ -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;
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
import { getCardLivenessPolicy } from "../constants.js";
|
||||
|
||||
/**
|
||||
* Whether a card is governed by a hide-until-verified policy (so it starts hidden
|
||||
* and must be PROBED to ever appear). Such cards can't rely on the viewport
|
||||
* IntersectionObserver — a `display:none` card never intersects, so the probe
|
||||
* must be kicked eagerly on register. Owner's own cards are always visible and
|
||||
* never gated.
|
||||
*/
|
||||
export function cardNeedsEagerLivenessProbe(cardLike) {
|
||||
const card = resolveCardElement(cardLike);
|
||||
if (!card || card.dataset.ownerIsViewer === "true") {
|
||||
return false;
|
||||
}
|
||||
const policy = getCardLivenessPolicy();
|
||||
if (policy === "hide-all") {
|
||||
return true;
|
||||
}
|
||||
if (policy === "hide-foreign") {
|
||||
return card.dataset.foreign === "true";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveCardElement(cardLike) {
|
||||
if (!cardLike) {
|
||||
return null;
|
||||
|
||||
@@ -2,7 +2,10 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import { updateVideoCardSourceVisibility } from "../js/utils/cardSourceVisibility.js";
|
||||
import {
|
||||
updateVideoCardSourceVisibility,
|
||||
cardNeedsEagerLivenessProbe,
|
||||
} from "../js/utils/cardSourceVisibility.js";
|
||||
import { setCardLivenessPolicy } from "../js/constants.js";
|
||||
import { VideoCard } from "../js/ui/components/VideoCard.js";
|
||||
|
||||
@@ -222,6 +225,31 @@ test("CARD_LIVENESS_POLICY=hide-all hides every pending non-owner card but never
|
||||
assert.equal(nativeCard.hidden, false, "card appears once a source verifies");
|
||||
});
|
||||
|
||||
// Hide-until-verified cards start display:none, so the viewport observer can't
|
||||
// fire for them — they must be eager-probed. This predicate gates that, and it
|
||||
// must mirror the visibility policy exactly so the right cards get probed.
|
||||
test("cardNeedsEagerLivenessProbe gates eager probing to hidden-until-verified cards", (t) => {
|
||||
const { document } = setupDom(t);
|
||||
t.after(() => setCardLivenessPolicy("show-pending"));
|
||||
|
||||
const foreign = makeCard(document, { foreign: "true" });
|
||||
const native = makeCard(document, { foreign: "false" });
|
||||
const owner = makeCard(document, { foreign: "true", owner: "true" });
|
||||
|
||||
setCardLivenessPolicy("show-pending");
|
||||
assert.equal(cardNeedsEagerLivenessProbe(foreign), false, "show-pending never eager-probes");
|
||||
assert.equal(cardNeedsEagerLivenessProbe(native), false);
|
||||
|
||||
setCardLivenessPolicy("hide-foreign");
|
||||
assert.equal(cardNeedsEagerLivenessProbe(foreign), true, "foreign card is eager-probed");
|
||||
assert.equal(cardNeedsEagerLivenessProbe(native), false, "native card stays viewport-gated");
|
||||
assert.equal(cardNeedsEagerLivenessProbe(owner), false, "owner card is never gated");
|
||||
|
||||
setCardLivenessPolicy("hide-all");
|
||||
assert.equal(cardNeedsEagerLivenessProbe(native), true, "hide-all eager-probes native too");
|
||||
assert.equal(cardNeedsEagerLivenessProbe(owner), false, "owner card is never gated");
|
||||
});
|
||||
|
||||
test("VideoCard.closeMoreMenu only restores focus when the trigger was expanded", (t) => {
|
||||
const { document } = setupDom(t);
|
||||
|
||||
|
||||
@@ -320,8 +320,17 @@ un-hides when WebTorrent flips green). The real gaps:
|
||||
`rootMargin`, so cards just below the fold verify before the user scrolls to
|
||||
them — makes hide-until-verified feel instant. Override:
|
||||
`window.__BITVID_LIVENESS_PREFETCH_MARGIN__`.
|
||||
- [ ] **DECIDE after live A/B**: pick the default policy that feels best
|
||||
(recommend `hide-foreign`) and set `CARD_LIVENESS_POLICY` in instance-config.
|
||||
- [x] **Deadlock fixed (2026-06-24).** First hide-foreign test made ALL foreign/
|
||||
NIP-71 content vanish: 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 → hidden forever.
|
||||
Fix: `cardNeedsEagerLivenessProbe()` + eager-probe such cards on
|
||||
`onCardRegister` (fires regardless of visibility) in both `gridHealth` and
|
||||
`urlHealthObserver`, instead of waiting for an intersection that can't happen.
|
||||
Still concurrency-capped/priority-ordered; show-pending unchanged. Default is
|
||||
now `hide-foreign`.
|
||||
- [ ] **DECIDE after live A/B**: confirm `hide-foreign` feels right (foreign cards
|
||||
now verify-then-appear); else flip `CARD_LIVENESS_POLICY` in instance-config.
|
||||
|
||||
### 18. Embed button / modal on the video player does not open — FIXED 2026-06-23
|
||||
- [x] **Root cause: event-name mismatch.** `VideoModal.handleEmbedRequest`
|
||||
|
||||
Reference in new issue
Block a user