fix: cancel orphaned queued URL media probes (client-reliability-4)

This commit is contained in:
thePR0M3TH3AN committed 2026-10-19 23:17:03 -04:00
1 parent d9bd2df6e8
commit ce1381f500
7 files changed
+686 -44

No files matched your search

+77
View File
@@ -760,3 +760,80 @@ test_integrity_note:
```
Duplicate wrapper and unconditional listener release variants each fail SCN-shared-badge-listener-lifetime. Removing the post-checking membership guard fails SCN-checking-callback-disposal (actual starts1 vs expected0). These3 additional variants exit1 with1 failed/0 cancelled and are reverted; total14 killed variants for11 lifecycle scenarios. Original source/priority/observer expectations remain unchanged.
## client-reliability-4 — queued URL media ownership
```yaml
test_integrity_note:
change_type: ["new_tests"]
scenarios:
- id: SCN-url-last-owner
given: "four cards, three active media and one queued"
when: "last registration is disposed and an active slot frees"
then: "queued promise settles null, no cache entry or obsolete media start"
- id: SCN-url-shared-owner
given: "two feeds share a queued event/source"
when: "one feed disposes"
then: "exactly one media probe serves the live badge; disposed connected badge stays checking"
- id: SCN-url-last-shared-owner
given: "two feeds share a queued event/source"
when: "both feeds dispose"
then: "queued promise settles null and never starts media"
- id: SCN-url-cancel-rerequest
given: "cancelled queued event/source has pending promise continuations"
when: "a new feed requests the same source immediately"
then: "new inflight record survives old settlement and live badge becomes healthy"
- id: SCN-url-cancel-mirror
given: "queued primary has a mirror"
when: "last owner disposes"
then: "cancelled work settles without mirror HEAD/media or stale UI/cache"
- id: SCN-url-registration-reuse
given: "hidden old feed queues an eager callback"
when: "dispose, reuse container, deliver old callback and old disposer"
then: "only the new event reaches onCheck and starts IO"
- id: SCN-url-active-cache
given: "active media belongs to a connected card"
when: "dispose its registration and complete active media"
then: "active result caches healthy, disposed badge stays checking, new card uses cache"
- id: SCN-url-hidden-reveal
given: "URL-only card is hidden pending health"
when: "eager media confirmation succeeds"
then: "card becomes visible with healthy badge"
- id: SCN-url-media-limit
given: "three active media probes plus FIFO queued work"
when: "slot releases while a new card enters"
then: "existing queued source runs first; active media stays three"
- id: SCN-url-mirror-order
given: "primary and two mirrors exist"
when: "primary errors then first mirror succeeds"
then: "primary then first mirror are requested; second mirror is untouched"
- id: SCN-url-legacy-callback
given: "connected badge lacks observer ownership"
when: "legacy caller invokes controller"
then: "media settles and healthy badge paints"
- id: SCN-url-cross-controller
given: "shared real inflight cache and two controllers"
when: "creator feed disposes while other feed remains"
then: "queued shared source starts once and other controller paints healthy"
observable_outcomes:
- "HEAD and native media IO starts, promise settlement, real cache/inflight records, connected badge and card visibility"
- "Observer callback emissions after disposal and container reuse"
determinism_controls:
- "jsdom cards, controllable IntersectionObserver, explicit loadeddata/error events and stubbed fetch boundary; real observer/controller/cache"
- "Unique event/source keys and task-turn drain; no live traffic, arbitrary sleeps, retries or relaxed assertions"
anti_cheat_rationale:
prevents:
- "hard-coded healthy or cancelled results"
- "testing a copied queue instead of real controller and observer"
- "cancelling shared live work or caching cancellation as offline"
- "stale connected DOM callbacks hidden by isConnected guards"
- "retry/sleep-based flake masking"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
All existing assertions preserved. `tests/url-health-lifecycle.test.mjs` adds12 scenarios. Mutations are temporary edits restored before gates. `python3 /tmp/item4-mutants.py` final run:13/13 variants naturally exit1 with named assertion failures (no timeouts); logs `/tmp/item4-mutant-*.log`, results `/tmp/item4-mutants.json`. No production test runner/gate changes.
Coverage grouping: no-unregister→last owner; cancel-shared→shared owner/cross-controller; reuse-cancelled-inflight→immediate retry/new inflight; cache-cancellation→last/shared owner/retry/mirror cache outcomes; mirror-fallthrough→cancelled primary; grouped stale guards→shared/active disposed UI (remove both redundant owner guards together; two equivalent variants); eager-callback-stale→registration reuse; no-eager-reveal→hidden reveal; concurrency-four→three-media cap/FIFO; reverse-mirrors→mirror ordering; break-legacy-consumer→callback compatibility; controller-local-records→shared cache ownership across controllers. Initial mutation pass exposed callback-emission and first-source gaps; new assertions were strengthened, then all mutants rejected. Initial no-unregister full-file run failed assertions but hung in another scenario awaiting its intentionally broken promise; final targeted last-owner variant naturally exits1. No timeout is counted as successful mutant evidence.
@@ -0,0 +1,7 @@
# client-reliability-4 implementation
- Existing observer unregister boundary owns per-badge lifecycle tokens, read by controller via WeakMap. Existing app/feed callback arguments unchanged; direct legacy controller consumers remain live until settlement.
- Module-scoped promise→ownership WeakMap mirrors shared real inflight cache across controllers. Last-owner release only cancels queued media jobs. Already-running media/cache behavior remains; no active HEAD/media/HLS/torrent abort.
- Queue resumes reserve a slot synchronously to preserve three-media cap. Cancellation settles with an explicit internal outcome propagated through mirrors and omitted from health cache. Existing state-cache promise identity guard preserves immediate re-request.
- Guards bind UI/RAF work to captured registration; late observer callbacks/eager microtasks and stale disposer cannot affect reused registrations.
- No app/shared observer/cache-module or policy/TTL edits. Scope/read-only predecessor evidence at77d486fb; dispatch implementation routing merged before code.
+2
View File
@@ -16,6 +16,8 @@
## client-reliability
- 2026-10-20: item4 routed implementation in URL observer/controller complete locally;12 lifecycle scenarios and13 reverted mutants, focused/lint/build pass. Queued last-owner work settles without cache/mirror/stale UI; shared owners, immediate retry and hidden reveal covered. Remaining: native browser smoke/full merged-tree gate/normal landing/closure; active abort/fallback/CPU unverified.
- 2026-10-20: client-reliability-4 investigation at4c54858c reproduces queued URL media start after feed disposal: active3/queued1 before and after disposal, then1 obsolete start(expected0); shared live-owner control deduplicates and receives healthy badge. Strict assertion exits1, diagnostic/control naturally exit0. No product/tests changed; awaiting dispatch fix routing. Active abort/browser/fallback/CPU unverified. Evidence: test_logs/TEST_LOG_2026-10-20_03-10-00_client-reliability-4.md.
- 2026-10-19: client-reliability-1 assigned: reproduce/classify and fix profile-modal suite hang without weakening assertions. Initial evidence: three isolated 60s timeouts (one clean environment), full baseline stopped at file 220/425; read docs/agents/prompts/client-reliability.md and claim the item. Scope excludes app.js and unrelated subsystems.
+117 -39
View File
@@ -1,17 +1,21 @@
import { getUrlHealthBadgeOwner } from "../urlHealthObserver.js";
import { SHORT_TIMEOUT_MS } from "../constants.js";
import {
isHlsUrl,
prefersNativeHls,
probeHlsManifest,
probeHlsManifest
} from "../services/hlsPlayback.js";
// Inflight cache is shared, so ownership also spans controller instances.
const urlProbeRecords = new WeakMap();
export default class UrlHealthController {
constructor({
state = {},
utils = {},
logger = {},
constants = {},
callbacks = {},
callbacks = {}
} = {}) {
this.state = state;
this.utils = utils;
@@ -44,7 +48,13 @@ export default class UrlHealthController {
`;
}
updateUrlHealthBadge(badgeEl, state, videoId) {
updateUrlHealthBadge(
badgeEl,
state,
videoId,
owner = getUrlHealthBadgeOwner(badgeEl)
) {
if (owner && !owner.active) return;
if (!badgeEl) {
return;
}
@@ -61,7 +71,7 @@ export default class UrlHealthController {
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => {
if (badgeEl.isConnected) {
this.updateUrlHealthBadge(badgeEl, state, videoId);
this.updateUrlHealthBadge(badgeEl, state, videoId, owner);
}
});
}
@@ -74,18 +84,19 @@ export default class UrlHealthController {
offline: "❌ CDN",
unknown: "⚠️ CDN",
timeout: "⚠️ CDN timed out",
checking: "⏳ CDN",
checking: "⏳ CDN"
};
const message =
state?.message ||
fallbackMessages[status] ||
fallbackMessages.checking;
state?.message || fallbackMessages[status] || fallbackMessages.checking;
const hadCompactMargin =
badgeEl.classList.contains("mt-sm") || badgeEl.classList.contains("mt-3");
badgeEl.dataset.urlHealthState = status;
const cardEl = badgeEl.closest(".card[data-video-id]");
if (cardEl && typeof this.utils.updateVideoCardSourceVisibility === "function") {
if (
cardEl &&
typeof this.utils.updateVideoCardSourceVisibility === "function"
) {
cardEl.dataset.urlHealthState = status;
this.utils.updateVideoCardSourceVisibility(cardEl);
}
@@ -104,7 +115,7 @@ export default class UrlHealthController {
offline: "critical",
unknown: "neutral",
timeout: "neutral",
checking: "neutral",
checking: "neutral"
};
const variant = variantMap[status];
if (variant) {
@@ -129,7 +140,7 @@ export default class UrlHealthController {
message,
lastCheckedAt: Number.isFinite(state?.lastCheckedAt)
? state.lastCheckedAt
: undefined,
: undefined
});
}
}
@@ -139,6 +150,8 @@ export default class UrlHealthController {
return;
}
const owner = getUrlHealthBadgeOwner(badgeEl) || { active: true };
if (!owner.active) return;
const eventId = video.id;
const trimmedUrl = typeof url === "string" ? url.trim() : "";
if (!trimmedUrl) {
@@ -157,6 +170,8 @@ export default class UrlHealthController {
this.updateUrlHealthBadge(badgeEl, { status: "checking" }, eventId);
if (!owner.active) return;
// Try the primary url plus any additional NIP-71 source mirrors, so one dead
// host doesn't mark a video offline when an alternate plays.
const sources = Array.isArray(video?.sources) ? video.sources : [];
@@ -164,7 +179,9 @@ export default class UrlHealthController {
const seenUrls = new Set();
for (const candidate of [
trimmedUrl,
...sources.map((s) => (s && typeof s.url === "string" ? s.url.trim() : "")),
...sources.map((s) =>
s && typeof s.url === "string" ? s.url.trim() : ""
)
]) {
if (candidate && !seenUrls.has(candidate)) {
seenUrls.add(candidate);
@@ -179,14 +196,21 @@ export default class UrlHealthController {
? getInFlight(eventId, trimmedUrl, probeOptions)
: null;
if (existingProbe) {
const existingRecord = existingProbe && urlProbeRecords.get(existingProbe);
if (existingProbe && !existingRecord?.cancelled) {
const release = existingRecord
? this.retainUrlProbe(existingRecord, owner)
: () => {};
existingProbe
.then((entry) => {
if (entry) {
this.updateUrlHealthBadge(badgeEl, entry, eventId);
release();
if (entry && owner.active) {
this.updateUrlHealthBadge(badgeEl, entry, eventId, owner);
}
})
.catch((err) => {
release();
this.logger.warn(
`[urlHealth] cached probe promise rejected for ${trimmedUrl}:`,
err
@@ -195,8 +219,14 @@ export default class UrlHealthController {
return;
}
const probePromise = this.probeUrlList(candidateUrls, probeOptions)
const record = { owners: new Set(), cancelled: false, cancelQueued: null };
const release = this.retainUrlProbe(record, owner);
const probePromise = this.probeUrlList(candidateUrls, {
...probeOptions,
lifecycle: record
})
.then((result) => {
if (record.cancelled || result?.outcome === "cancelled") return null;
const outcome = result?.outcome || "error";
let entry;
@@ -205,17 +235,17 @@ export default class UrlHealthController {
} else if (outcome === "opaque" || outcome === "unknown") {
entry = {
status: "unknown",
message: "⚠️ CDN",
message: "⚠️ CDN"
};
} else if (outcome === "timeout") {
entry = {
status: "timeout",
message: "⚠️ CDN timed out",
message: "⚠️ CDN timed out"
};
} else {
entry = {
status: "offline",
message: "❌ CDN",
message: "❌ CDN"
};
}
@@ -236,10 +266,11 @@ export default class UrlHealthController {
return entry;
})
.catch((err) => {
if (record.cancelled) return null;
this.logger.warn(`[urlHealth] probe failed for ${trimmedUrl}:`, err);
const entry = {
status: "offline",
message: "❌ CDN",
message: "❌ CDN"
};
if (typeof this.state.storeUrlHealth === "function") {
return this.state.storeUrlHealth(eventId, trimmedUrl, entry);
@@ -247,6 +278,7 @@ export default class UrlHealthController {
return entry;
});
urlProbeRecords.set(probePromise, record);
if (typeof this.state.setInFlightUrlProbe === "function") {
this.state.setInFlightUrlProbe(
eventId,
@@ -258,11 +290,13 @@ export default class UrlHealthController {
probePromise
.then((entry) => {
if (entry) {
this.updateUrlHealthBadge(badgeEl, entry, eventId);
release();
if (entry && owner.active) {
this.updateUrlHealthBadge(badgeEl, entry, eventId, owner);
}
})
.catch((err) => {
release();
this.logger.warn(
`[urlHealth] probe promise rejected post-cache for ${trimmedUrl}:`,
err
@@ -270,8 +304,22 @@ export default class UrlHealthController {
});
}
async probeUrlWithVideoElement(url, timeoutMs) {
const defaultTimeout = this.constants.URL_PROBE_TIMEOUT_MS || SHORT_TIMEOUT_MS;
retainUrlProbe(record, owner) {
record.owners.add(owner);
const onDispose = () => {
record.owners.delete(owner);
if (!record.owners.size) record.cancelQueued?.();
};
owner.releases?.add(onDispose);
return () => {
record.owners.delete(owner);
owner.releases?.delete(onDispose);
};
}
async probeUrlWithVideoElement(url, timeoutMs, lifecycle) {
const defaultTimeout =
this.constants.URL_PROBE_TIMEOUT_MS || SHORT_TIMEOUT_MS;
const effectiveTimeout =
Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : defaultTimeout;
@@ -290,20 +338,49 @@ export default class UrlHealthController {
const defaultTimeout =
this.constants.URL_PROBE_TIMEOUT_MS || SHORT_TIMEOUT_MS;
const effective =
Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : defaultTimeout;
Number.isFinite(timeoutMs) && timeoutMs > 0
? timeoutMs
: defaultTimeout;
const outcome = await probeHlsManifest(trimmed, effective);
return { outcome };
}
if (lifecycle && !lifecycle.owners.size) {
lifecycle.cancelled = true;
return { outcome: "cancelled" };
}
if (this.activeVideoProbes >= this.MAX_CONCURRENT_VIDEO_PROBES) {
await new Promise((resolve) => {
this.videoProbeQueue.push(resolve);
const acquired = await new Promise((resolve) => {
const job = {
resume: () => {
if (lifecycle) lifecycle.cancelQueued = null;
// Reserve the slot synchronously, before another caller can use it.
this.activeVideoProbes++;
resolve(true);
}
};
this.videoProbeQueue.push(job);
if (lifecycle) {
lifecycle.cancelQueued = () => {
const index = this.videoProbeQueue.indexOf(job);
if (index < 0) return;
this.videoProbeQueue.splice(index, 1);
lifecycle.cancelled = true;
lifecycle.cancelQueued = null;
resolve(false);
};
}
});
if (!acquired) return { outcome: "cancelled" };
} else {
this.activeVideoProbes++;
}
this.activeVideoProbes++;
try {
if (lifecycle && !lifecycle.owners.size) {
lifecycle.cancelled = true;
return { outcome: "cancelled" };
}
return await new Promise((resolve) => {
const video = document.createElement("video");
let settled = false;
@@ -369,9 +446,7 @@ export default class UrlHealthController {
this.activeVideoProbes--;
if (this.videoProbeQueue.length > 0) {
const next = this.videoProbeQueue.shift();
if (typeof next === "function") {
next();
}
next.resume();
}
}
}
@@ -396,6 +471,7 @@ export default class UrlHealthController {
continue;
}
const outcome = result?.outcome;
if (outcome === "cancelled") return result;
if (outcome === "ok") {
return { outcome: "ok", url };
}
@@ -413,7 +489,8 @@ export default class UrlHealthController {
}
const confirmPlayable = options?.confirmPlayable === true;
const defaultTimeout = this.constants.URL_PROBE_TIMEOUT_MS || SHORT_TIMEOUT_MS;
const defaultTimeout =
this.constants.URL_PROBE_TIMEOUT_MS || SHORT_TIMEOUT_MS;
const urlHealthConstants = this.constants.urlHealthConstants || {};
const confirmWithVideoElement = async () => {
@@ -431,7 +508,8 @@ export default class UrlHealthController {
try {
const result = await this.probeUrlWithVideoElement(
trimmed,
timeoutMs
timeoutMs,
options.lifecycle
);
if (result && result.outcome) {
return result;
@@ -473,8 +551,8 @@ export default class UrlHealthController {
method: "HEAD",
mode: "no-cors",
cache: "no-store",
signal: controller ? controller.signal : undefined,
}),
signal: controller ? controller.signal : undefined
})
];
if (Number.isFinite(defaultTimeout) && defaultTimeout > 0) {
@@ -543,7 +621,7 @@ export default class UrlHealthController {
}
return {
outcome: "bad",
status: response.status,
status: response.status
};
}
@@ -552,7 +630,7 @@ export default class UrlHealthController {
if (playbackCheck.outcome === "ok") {
return {
...playbackCheck,
status: response.status,
status: response.status
};
}
return playbackCheck;
@@ -560,7 +638,7 @@ export default class UrlHealthController {
return {
outcome: "ok",
status: response.status,
status: response.status
};
}
}
+36 -5
View File
@@ -9,6 +9,13 @@ import { cardNeedsEagerLivenessProbe } from "./utils/cardSourceVisibility.js";
const ROOT_MARGIN = getLivenessProbePrefetchMargin();
const THRESHOLD = 0.25;
// Badge ownership crosses the existing callback boundary without requiring
// callers to forward a new argument through the app/feed controllers.
const badgeOwners = new WeakMap();
export function getUrlHealthBadgeOwner(badgeEl) {
return badgeOwners.get(badgeEl);
}
const urlCardObserver = createCardObserver({
rootMargin: ROOT_MARGIN,
threshold: THRESHOLD,
@@ -17,27 +24,44 @@ const urlCardObserver = createCardObserver({
return false;
}
const isIntersecting = Boolean(entry.isIntersecting);
const ratio = typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0;
const ratio =
typeof entry.intersectionRatio === "number" ? entry.intersectionRatio : 0;
return isIntersecting && ratio > 0;
},
createState: () => ({ onCheck: null }),
createState: () => ({ onCheck: null, active: true, owners: new Map() }),
// 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 }) => {
const badge = card.querySelector("[data-url-health-state]");
const owner = { active: true, releases: new Set() };
state.owners.set(card, owner);
if (badge) badgeOwners.set(badge, owner);
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));
}
},
onCardUnregister: ({ card, state }) => {
const owner = state.owners.get(card);
if (!owner) return;
owner.active = false;
for (const release of owner.releases) release();
owner.releases.clear();
state.owners.delete(card);
},
onCardVisible: ({ card, state }) => {
triggerUrlCheck(card, state);
},
}
});
function triggerUrlCheck(card, state) {
if (!state || typeof state.onCheck !== "function") {
if (
!state?.active ||
!state.owners.get(card)?.active ||
typeof state.onCheck !== "function"
) {
return;
}
@@ -82,5 +106,12 @@ export function attachUrlHealthBadges(container, onCheck) {
if (state && typeof onCheck === "function") {
state.onCheck = onCheck;
}
return () => urlCardObserver.disconnect(container);
return () => {
if (!state?.active) return;
state.active = false;
// A stale disposer must never disconnect a newer registration.
if (urlCardObserver.getState(container) === state) {
urlCardObserver.disconnect(container);
}
};
}
@@ -0,0 +1,9 @@
# client-reliability-4 precommit gates
- `env -u NODE_TEST_CONTEXT node tests/url-health-lifecycle.test.mjs`: exit0,12/12 scenarios,0 failures/cancellations/skips. Inherited NODE_TEST_CONTEXT=child-v8 makes --test output misleading; direct self-executing test file yields named scenarios. No runner edit.
- `env -u NODE_TEST_CONTEXT node scripts/run-unit-tests.mjs url-health health-disposal`: exit0,4 files. New lifecycle12, URL controller4, multi-source4, view health disposal2 =22 scenarios.
- `python3 /tmp/item4-mutants.py`: final13/13 restored mutants naturally exit1 with named assertion failures, no timeout counted. Coverage in TEST_INTEGRITY.md; initial surviving callback/mirror variants led to stronger assertions before final run.
- `npm run lint`: sandbox exit2 at git grep EPERM; same command with approved subprocess access exits0,11 stages. No lint relaxation.
- `npm run build`: exit0, including deploy-artifact verification; pre-existing Browserslist age warning, dependencies unchanged.
- Browser ephemeral server initially denied listen EPERM; `node /tmp/item4-browser.mjs` with approved local access exits0: Chromium+Firefox each0 obsolete starts, healthy shared badge/disposed badge checking, hidden reveal, hosted/torrent source-health visibility pass,0 external requests/page errors. Owned loopback server/browser closed; preview3000 untouched. Full merged-tree suite and normal queue/durable ancestry still required.
- No CPU/RSS or actual playback/live peers/fallback qualification. Only external IO virtualized; browser cache boundary uses in-memory service virtualization, unit regression uses actual state/cache module.
+438
View File
@@ -0,0 +1,438 @@
import test from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
// Resolve real cache dependencies with installed Node packages before DOM setup.
import * as cache from "../js/state/cache.js";
import UrlHealthController from "../js/ui/urlHealthController.js";
import { attachUrlHealthBadges } from "../js/urlHealthObserver.js";
import {
setCardLivenessPolicy,
getCardLivenessPolicy
} from "../js/constants.js";
import { updateVideoCardSourceVisibility } from "../js/utils/cardSourceVisibility.js";
const flush = () => new Promise((resolve) => setImmediate(resolve));
let sequence = 0;
function fixture(t) {
const dom = new JSDOM("<!doctype html><body></body>", {
url: "https://example.invalid"
});
const names = [
"window",
"document",
"HTMLElement",
"localStorage",
"fetch",
"IntersectionObserver",
"requestAnimationFrame"
];
const saved = names.map((key) => [
key,
Object.getOwnPropertyDescriptor(globalThis, key)
]);
Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
HTMLElement: dom.window.HTMLElement,
localStorage: dom.window.localStorage
});
const policy = getCardLivenessPolicy();
setCardLivenessPolicy("show-pending");
const observers = [],
media = [],
heads = [],
promises = [],
warnings = [],
registrations = [],
visibility = [],
checks = [];
const id = (name) => `url-lifecycle-${++sequence}-${name}`;
const url = (name) => `https://media.invalid/${name}.mp4`;
globalThis.IntersectionObserver = class {
constructor(callback) {
this.callback = callback;
this.cards = [];
observers.push(this);
}
observe(card) {
this.cards.push(card);
}
takeRecords() {
return [];
}
disconnect() {
this.disconnected = true;
}
show() {
this.callback(
this.cards.map((target) => ({
target,
isIntersecting: true,
intersectionRatio: 1
}))
);
}
};
globalThis.fetch = async (source, options) => {
heads.push({ source, options });
return { ok: true, status: 200, type: "basic" };
};
dom.window.HTMLMediaElement.prototype.load = function () {
if (this.getAttribute("src") && !media.includes(this)) media.push(this);
};
dom.window.HTMLMediaElement.prototype.pause = function () {};
const controller = new UrlHealthController({
state: {
...cache,
setInFlightUrlProbe(...args) {
promises.push(args[2]);
return cache.setInFlightUrlProbe(...args);
}
},
logger: {
warn(...args) {
warnings.push(args);
}
},
utils: {
updateVideoCardSourceVisibility(card) {
visibility.push(card);
updateVideoCardSourceVisibility(card);
}
}
});
function feed(
videos,
{ container = document.createElement("section") } = {}
) {
container.replaceChildren();
for (const video of videos) {
const card = document.createElement("article");
card.className = "card";
card.dataset.videoId = video.id;
card.dataset.urlHealthEventId = video.id;
card.dataset.urlHealthUrl = encodeURIComponent(video.url);
card.dataset.urlHealthState = "checking";
card.dataset.streamHealthState = "unsupported";
const badge = document.createElement("span");
badge.dataset.urlHealthState = "checking";
card.append(badge);
container.append(card);
updateVideoCardSourceVisibility(card);
}
document.body.append(container);
const registration = { container, videos };
registration.dispose = attachUrlHealthBadges(
container,
({ badgeEl, url, eventId }) => {
checks.push(eventId);
controller.handleUrlHealthBadge({
video: videos.find((v) => v.id === eventId),
url,
badgeEl
});
}
);
registration.observer = observers.at(-1);
registration.cards = [...container.children];
registration.badges = registration.cards.map(
(card) => card.firstElementChild
);
registrations.push(registration);
return registration;
}
const video = (name) => {
const key = id(name);
return { id: key, url: url(key) };
};
const done = (element, event = "loadeddata") =>
element.dispatchEvent(new dom.window.Event(event));
async function finishAll() {
// Event-driven drain: each iteration completes the currently created media.
// No wall-clock delay or retry of a failed assertion.
let handled = 0;
while (handled < media.length) {
for (const element of media.slice(handled)) {
done(element);
handled++;
}
await flush();
}
await Promise.all(promises);
await flush();
}
t.after(async () => {
for (const registration of registrations) registration.dispose();
await finishAll();
dom.window.close();
setCardLivenessPolicy(policy);
for (const [key, descriptor] of saved) {
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
else delete globalThis[key];
}
});
return {
dom,
controller,
media,
heads,
promises,
warnings,
visibility,
checks,
feed,
video,
done,
finishAll
};
}
async function saturated(f) {
const videos = ["a", "b", "c", "queued"].map(f.video);
const feed = f.feed(videos);
feed.observer.show();
await flush();
assert.equal(f.media.length, 3);
assert.equal(f.heads.length, 4);
return { ...feed, videos };
}
test("last feed disposal settles queued URL work without an obsolete media start or negative cache", async (t) => {
const f = fixture(t),
old = await saturated(f),
queuedPromise = f.promises[3];
old.dispose();
old.container.remove();
await flush();
f.done(f.media[0]);
await flush();
assert.equal(f.media.length, 3);
assert.equal(await queuedPromise, null);
assert.equal(
cache.getCachedUrlHealth(old.videos[3].id, old.videos[3].url),
null
);
assert.equal(
cache.getInFlightUrlProbe(old.videos[3].id, old.videos[3].url, {
confirmPlayable: true
}),
null
);
await f.finishAll();
assert.equal(f.controller.activeVideoProbes, 0);
assert.equal(f.warnings.length, 0);
});
test("a queued shared URL survives disposal of one owner and updates only the live badge", async (t) => {
const f = fixture(t),
old = await saturated(f),
live = f.feed([old.videos[3]]);
live.observer.show();
await flush();
assert.equal(f.promises.length, 4);
assert.equal(f.heads.length, 4);
old.dispose(); // Keep DOM connected: stale badge guard must not rely on isConnected.
f.done(f.media[0]);
await flush();
assert.equal(f.media.length, 4);
f.done(f.media[3]);
await flush();
assert.equal(live.badges[0].dataset.urlHealthState, "healthy");
assert.equal(old.badges[3].dataset.urlHealthState, "checking");
});
test("disposing the final shared owner cancels queued work", async (t) => {
const f = fixture(t),
old = await saturated(f),
live = f.feed([old.videos[3]]);
live.observer.show();
await flush();
old.dispose();
live.dispose();
await flush();
f.done(f.media[0]);
await flush();
assert.equal(f.media.length, 3);
assert.equal(await f.promises[3], null);
});
test("same source can re-request immediately after cancellation without old settlement deleting its inflight record", async (t) => {
const f = fixture(t),
old = await saturated(f),
v = old.videos[3];
old.dispose();
const live = f.feed([v]);
live.observer.show(); // Deliberately before cancelled promise continuations.
const replacement = f.promises[4];
assert.ok(replacement);
assert.notEqual(replacement, f.promises[3]);
await flush();
assert.equal(
cache.getInFlightUrlProbe(v.id, v.url, { confirmPlayable: true }),
replacement
);
assert.equal(cache.getCachedUrlHealth(v.id, v.url), null);
f.done(f.media[0]);
await flush();
assert.equal(f.media.length, 4);
f.done(f.media[3]);
await flush();
assert.equal(live.badges[0].dataset.urlHealthState, "healthy");
});
test("cancelled queued primary does not fall through to a mirror or update a connected disposed badge", async (t) => {
const f = fixture(t),
busy = f.feed(["a", "b", "c"].map(f.video));
busy.observer.show();
await flush();
const v = f.video("mirrored");
v.sources = [{ url: "https://mirror.invalid/alternate.mp4" }];
const old = f.feed([v]);
old.observer.show();
await flush();
old.dispose();
await flush();
assert.equal(f.heads.length, 4);
assert.equal(await f.promises[3], null);
assert.equal(old.badges[0].dataset.urlHealthState, "checking");
await f.finishAll();
assert.equal(f.media.length, 3);
assert.equal(f.heads.length, 4);
});
test("disposal blocks queued eager microtasks and late observer callbacks, including after container reuse", async (t) => {
const f = fixture(t);
setCardLivenessPolicy("hide-all");
const old = f.feed([f.video("old")]);
old.dispose();
const live = f.feed([f.video("new")], { container: old.container });
old.dispose();
old.observer.show();
await flush();
assert.deepEqual(f.checks, [live.videos[0].id]);
assert.equal(f.heads.length, 1);
assert.equal(f.heads[0].source, live.videos[0].url);
assert.equal(f.media.length, 1);
f.done(f.media[0]);
await flush();
assert.equal(live.badges[0].dataset.urlHealthState, "healthy");
});
test("active media retain prior result/cache behavior while disposed connected badges stay unchanged", async (t) => {
const f = fixture(t),
v = f.video("active"),
old = f.feed([v]);
old.observer.show();
await flush();
old.dispose();
assert.equal(f.media[0].src, v.url);
f.done(f.media[0]);
await flush();
assert.equal(cache.getCachedUrlHealth(v.id, v.url).status, "healthy");
assert.equal(old.badges[0].dataset.urlHealthState, "checking");
const live = f.feed([v]);
live.observer.show();
await flush();
assert.equal(live.badges[0].dataset.urlHealthState, "healthy");
assert.equal(f.heads.length, 1);
});
test("hidden URL-only card probes eagerly and reveals after healthy media confirmation", async (t) => {
const f = fixture(t);
setCardLivenessPolicy("hide-all");
const live = f.feed([f.video("hidden")]);
assert.equal(live.cards[0].hidden, true);
await flush();
assert.equal(f.heads.length, 1);
assert.equal(f.media.length, 1);
f.done(f.media[0]);
await flush();
assert.equal(live.cards[0].hidden, false);
assert.equal(live.cards[0].dataset.sourceVisibility, "visible");
});
test("three-media FIFO limit holds while a freed slot and a newly visible card compete", async (t) => {
const f = fixture(t),
old = await saturated(f);
const extra = f.feed([f.video("extra")]);
f.done(f.media[0]);
extra.observer.show();
await flush();
assert.equal(f.media.length, 4);
assert.equal(f.media[3].src, old.videos[3].url);
assert.equal(f.controller.activeVideoProbes, 3);
f.done(f.media[1]);
await flush();
assert.equal(f.media.length, 5);
assert.equal(f.media[4].src, extra.videos[0].url);
assert.equal(f.controller.activeVideoProbes, 3);
});
test("live mirrors stay ordered and stop after first playable source", async (t) => {
const f = fixture(t),
v = f.video("primary");
v.sources = [
{ url: "https://mirror.invalid/one.mp4" },
{ url: "https://mirror.invalid/two.mp4" }
];
const live = f.feed([v]);
live.observer.show();
await flush();
assert.equal(f.heads.length, 1);
assert.equal(f.heads[0].source, v.url);
f.done(f.media[0], "error");
await flush();
assert.equal(f.heads.length, 2);
assert.equal(f.heads[1].source, v.sources[0].url);
f.done(f.media[1]);
await flush();
assert.equal(f.heads.length, 2);
assert.equal(live.badges[0].dataset.urlHealthState, "healthy");
});
test("legacy direct callback consumers without observer ownership still settle and paint", async (t) => {
const f = fixture(t),
v = f.video("legacy"),
badge = document.createElement("span");
document.body.append(badge);
f.controller.handleUrlHealthBadge({ video: v, url: v.url, badgeEl: badge });
await flush();
f.done(f.media[0]);
await flush();
assert.equal(badge.dataset.urlHealthState, "healthy");
});
test("shared inflight ownership also survives a second controller using the same cache", async (t) => {
const f = fixture(t),
old = await saturated(f),
v = old.videos[3];
const controller = new UrlHealthController({
state: cache,
logger: { warn() {} }
});
// Preserve the production observer callback shape while selecting another controller.
const container = document.createElement("section"),
card = document.createElement("article"),
badge = document.createElement("span");
card.className = "card";
card.dataset.videoId = v.id;
card.dataset.urlHealthEventId = v.id;
card.dataset.urlHealthUrl = encodeURIComponent(v.url);
badge.dataset.urlHealthState = "checking";
card.append(badge);
container.append(card);
document.body.append(container);
const dispose = attachUrlHealthBadges(container, ({ badgeEl, url }) =>
controller.handleUrlHealthBadge({ video: v, url, badgeEl })
);
t.after(dispose);
// The fixture observer class records new observations on the returned object.
controller.handleUrlHealthBadge({ video: v, url: v.url, badgeEl: badge });
old.dispose();
f.done(f.media[0]);
await flush();
assert.equal(f.media.length, 4);
f.done(f.media[3]);
await flush();
assert.equal(badge.dataset.urlHealthState, "healthy");
assert.equal(f.heads.length, 4);
});