land: area/client-reliability 280f043a66 (client-reliability)

This commit is contained in:
thePR0M3TH3AN committed 2026-10-23 08:51:52 -04:00
commit 83fd2495b4
10 files changed
+706 -16

No files matched your search

@@ -0,0 +1 @@
HLS-only implementation perdispatch20:55:13Z/confirmed newestRULE. Controller passes optional lifecycle; manifesthelper cancellation covers fetch+body, synchronous invalidation/promptcancelled/abort/identitysafehook+timercleanup. No engine/player/session/HEAD/media/torrent/RangeGET changes. New35tests+adjacent94=129pass/6files;19isolatedmutantsfailnaturally/cancelled0. Final lint pending; record then commit+merge currentunstable,READY for serialized full/browser/build/normal queue. No expensive stage/submission without grant.
@@ -0,0 +1,4 @@
- Explicit dispatch20:55:13Z Stage3 HLS scope extension and helper/controller ownership; keep broaderitem9OPEN. Existing HEAD/media/engine/player/nativepreference behavior and standalone manifest callers preserved.
- Pass optional lifecycle as third helper argument; helper owns its own hook/controller/timer through fetch AND response.text, races logical cancellation independently of AbortController cooperation. Native media remains on existing path.
- Test actual observer/controller/cache/helper with controlled IO/clock boundaries, new unique suite only; existing tests/expectations unchanged. Baseline35newcases13pass/22fail before fix (isolated copies); original12mode5fail/7pass retained.
- Mutants isolate copies in/tmp,never mutate worktree/shared modules. Initial retainedtimer mutant hit engineclearTimeout outside manifesthelper;corrected exacthelper suffix and final19allfail naturally. Formatting outside manifesthelper restored to keep engine read-only.
@@ -7,5 +7,5 @@
"addedOn": "2026-10-22",
"filedAt": "fb54cea3a2ba99b4860421724a5050af6b97c293",
"brief": "docs/agents/CAMPAIGN-2026-10-22.md",
"evidence": "HEAD-only fix e9c960ed/merged09f52bdd durably landed f41ac656 normalgateexit0,435discovered/435unique starts missingextra0,ancestry0;build/artifactexit0;controlled native Chromium+Firefox lifecycle PASS with synthetic loadeddata/publicdeny. Prior60focused/16mutants/lint11. Resources released. HLS/torrent/RangeGET separately OPEN; no wholeStage3 claim. test_logs/TEST_LOG_2026-10-22_20-52-00_client-reliability-9-browser.md HLS read-only atd04d501e:12modes/7controls pass/5cancellationfail/0cancelled/naturalexit1; GET/body finalowner retains timer/unsettled,rejoin old,latecache. Await explicit controller+HLShelper routing. test_logs/TEST_LOG_2026-10-22_20-54-00_client-reliability-9-hls-investigation.md"
"evidence": "HEAD-only fix e9c960ed/merged09f52bdd durably landed f41ac656 normalgateexit0,435discovered/435unique starts missingextra0,ancestry0;build/artifactexit0;controlled native Chromium+Firefox lifecycle PASS with synthetic loadeddata/publicdeny. Prior60focused/16mutants/lint11. Resources released. HLS/torrent/RangeGET separately OPEN; no wholeStage3 claim. test_logs/TEST_LOG_2026-10-22_20-52-00_client-reliability-9-browser.md HLS read-only atd04d501e:12modes/7controls pass/5cancellationfail/0cancelled/naturalexit1; GET/body finalowner retains timer/unsettled,rejoin old,latecache. Await explicit controller+HLShelper routing. test_logs/TEST_LOG_2026-10-22_20-54-00_client-reliability-9-hls-investigation.md HLS explicitlyrouted20:55:13Z;minimal controllerhandoff+helperfetch/bodycancel implemented;new35+existing94=129pass/6files,19isolatedmutantsnaturallyfail/cancelled0,lint11exit0;noexistingtestsmodified. READY for serialized full/build/browser/normal slot after candidatecommit/merge; notsubmitted. test_logs/TEST_LOG_2026-10-22_21-03-00_client-reliability-9-hls-ready.md"
}
@@ -0,0 +1,11 @@
# Active HLS manifest owner cancellation
Source: docs/performance-efficiency-plan.md Stage3, PROGRAM campaign priority2, explicit dispatch mailbox2026-10-22T20:55:13Z and newest RULE-CHANGES HLS routing. Dispatch independently reproduced actual12mode baseline7pass/5fail atd04d501e.
Old scoped behavior: queued/media/HEAD ownership cancellation does not reach the HLS manifest helper. Final-owner disposal retains manifest GET/body-read/timer and unsettled observer work; same-source rejoin inherits the old request and late success can cache.
New HLS health-probe contract: last registered owner disposal synchronously invalidates that work, requests transport abort where available and promptly settles the observer chain null even if fetch/body ignore abort or AbortController is absent. Release manifest timer and only that probe's hook once. Pre-cancelled work starts no GET. Fresh same-turn/later same-source owners start fresh identity; old responses/errors/timers/finalizers cannot change replacement/cache/badges/media/mirrors.
Preserve shared/cross-controller and ownerless legacy consumers, live success/error/bad-body/timeout, existing helper calls without lifecycle, native-HLS preference and existing media/HEAD/FIFO behavior. No existing assertion changed;35 new cases tighten uncovered HLS ownership behavior. This is a scope extension, not retroactive qualification by prior HEAD/media fixes.
Only controller HLS lifecycle argument and manifest-probe optional cancellation plumbing change. HLS engine/player/session/config, app, torrent and RangeGET stay outside this slice. No real playback/network-byte/browser/CPU/wholeStage3 qualification from unit evidence.
+41 -14
View File
@@ -378,16 +378,33 @@ export async function attachHlsSource(
* native playback needs no CORS, so the caller should keep using the element
* probe there.
*
* @returns {Promise<"ok"|"error"|"timeout">}
* Optional card-probe lifecycle owns cancellation through both fetch and body
* reading. Callers without a lifecycle retain the standalone probe behavior.
* @returns {Promise<"ok"|"error"|"timeout"|"cancelled">}
*/
export async function probeHlsManifest(url, timeoutMs = 4000) {
export async function probeHlsManifest(url, timeoutMs = 4000, lifecycle) {
const trimmed = typeof url === "string" ? url.trim() : "";
if (!trimmed || typeof fetch !== "function") {
return "error";
}
if (lifecycle && (lifecycle.cancelled || !lifecycle.owners.size)) {
lifecycle.cancelled = true;
return "cancelled";
}
const controller =
typeof AbortController === "function" ? new AbortController() : null;
let resolveCancellation;
const cancellation = new Promise((resolve) => {
resolveCancellation = resolve;
});
const cancelActive = () => {
// Invalidate synchronously so a same-turn owner starts a fresh probe.
lifecycle.cancelled = true;
resolveCancellation("cancelled");
controller?.abort();
};
if (lifecycle) lifecycle.cancelActive = cancelActive;
let timedOut = false;
const timeoutId =
Number.isFinite(timeoutMs) && timeoutMs > 0
@@ -397,22 +414,32 @@ export async function probeHlsManifest(url, timeoutMs = 4000) {
}, timeoutMs)
: null;
try {
const response = await fetch(trimmed, {
method: "GET",
cache: "no-store",
signal: controller?.signal,
});
if (!response.ok) {
return "error";
const request = async () => {
try {
const response = await fetch(trimmed, {
method: "GET",
cache: "no-store",
signal: controller?.signal
});
if (!response.ok) {
return "error";
}
const text = await response.text();
return text.trimStart().startsWith("#EXTM3U") ? "ok" : "error";
} catch (err) {
return timedOut ? "timeout" : "error";
}
const text = await response.text();
return text.trimStart().startsWith("#EXTM3U") ? "ok" : "error";
} catch (err) {
return timedOut ? "timeout" : "error";
};
try {
// Race the whole request, including response.text(): a transport or body
// reader may ignore abort, but obsolete card work must still settle.
return await Promise.race([request(), cancellation]);
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (lifecycle?.cancelActive === cancelActive) {
lifecycle.cancelActive = null;
}
}
}
+1 -1
View File
@@ -344,7 +344,7 @@ export default class UrlHealthController {
Number.isFinite(timeoutMs) && timeoutMs > 0
? timeoutMs
: defaultTimeout;
const outcome = await probeHlsManifest(trimmed, effective);
const outcome = await probeHlsManifest(trimmed, effective, lifecycle);
return { outcome };
}
@@ -0,0 +1,74 @@
# HLS health lifecycle integrity
```yaml
test_integrity_note:
change_type: [new_tests, spec_correction]
scenarios:
- id: SCN-hls-owner-cancel
given: "Actual observer/controller/helper with pending manifest fetch or body read"
when: "Final owner disposes, including duplicate disposal and ignored/missing abort"
then: "Prompt null, aborted when supported, timer cleanup once, no obsolete cache or mirror or badge"
- id: SCN-hls-replacement
given: "Cancelled HLS work and new same-source observer in same turn or later"
when: "Old success, error or timer fires"
then: "Fresh request/inflight survives; only current owner receives healthy cache and badge"
- id: SCN-hls-shared-legacy
given: "Two owners, cross-controller owners or ownerless legacy consumer in fetch/body phase"
when: "One registered owner disposes"
then: "One live GET, no abort, live healthy cache/badge and no disposed badge write"
- id: SCN-hls-live-contract
given: "Live HLS card with controlled response or timeout"
when: "Good manifest, HTTP error, invalid body or timeout completes"
then: "Existing healthy/offline/timeout and cleanup semantics preserved"
- id: SCN-hls-boundary
given: "Cancellation at HEAD handoff, native-support check or helper prestart"
when: "Ownership disappears"
then: "No manifest starts; native preference retains existing cancellable media behavior"
- id: SCN-hls-hook-cleanup
given: "Helper hook active or superseded by successor"
when: "Cancellation or completion finalizes"
then: "Own timer/hook removed once; successor hook preserved"
observable_outcomes:
- "Fetch count, abort signal, promise settlement, timers, cache, inflight identity and visible badges"
- "No obsolete mirror/media start; active media slots and queue return to zero"
determinism_controls:
- "Real observer/controller/cache/helper with JSDOM; controlled external fetch/body/viewport/timers"
- "Microtask/event-loop continuation drain only, no arbitrary sleeps, retries, force exits or public network"
anti_cheat_rationale:
prevents:
- "Abort signal without prompt settlement"
- "Fetch-only cancellation that leaves body reader pending"
- "Same-source fresh observer inheriting obsolete work"
- "Cancelling shared or legacy live owners"
- "Late cache or badge writes and replacement deletion"
- "Unconditional healthy response and lost live timeout/native branch"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
Spec extension documented in docs/spec_changes/2026-10-22-client-reliability-9-active-hls.md. No existing tests/expectations changed. Original actual12mode baseline and initial fixture corrections are retained in20-54 investigation note; new35case suite against original isolated modules produces13pass/22fail/cancelled0.
Grouped mutant coverage: six final-owner cases reject hook/abort/prompt/timer/cache failures; twelve replacement cases reject sync invalidation and stale cache finalizer; six shared/cross/legacy cases reject lost shared owner; four live controls reject corrupted success/status/body/timeout; handoff/native/prestart cases reject missing handoff/guard/native branch; hook cases reject absent cleanup and deleting successor. Each group has failing mutants; all35 cases covered without changing expectations.
Final19 isolated variants below naturally exit1 with assertion failures and0cancelled. Source copies live in/tmp; actual worktree modules never mutated. Initial timer mutant accidentally targeted engine cleanup, survived, corrected to manifest-helper suffix; final result rejects timer retention. Baseline variant includes absent API TypeErrors in direct-hook cases as well as22 total failures; primary lifecycle cases fail assertions.
- baseline: exit1, fail22, cancelled0
- no-handoff: exit1, fail19, cancelled0
- no-helper-hook: exit1, fail19, cancelled0
- no-cancel-abort: exit1, fail5, cancelled0
- no-prompt-settlement: exit1, fail18, cancelled0
- no-sync-invalidation: exit1, fail7, cancelled0
- retained-timer: exit1, fail30, cancelled0
- no-hook-cleanup: exit1, fail1, cancelled0
- successor-hook-deleted: exit1, fail1, cancelled0
- body-hook-lost: exit1, fail10, cancelled0
- no-prestart-guard: exit1, fail3, cancelled0
- cache-on-cancel: exit1, fail21, cancelled0
- lost-shared-owner: exit1, fail6, cancelled0
- native-branch-lost: exit1, fail2, cancelled0
- healthy-cache-corrupted: exit1, fail19, cancelled0
- bad-status-healthy: exit1, fail1, cancelled0
- bad-body-healthy: exit1, fail1, cancelled0
- no-timeout-abort: exit1, fail1, cancelled0
- stale-inflight-finalizer: exit1, fail6, cancelled0
@@ -0,0 +1,9 @@
# HLS-only cancellation candidate readiness
- Parent HEADfb0e671d4da534a52d834cb761c285b862d86889 contains docs-only investigation over runtime d04d501e. Dispatch20:55:13Z explicitly routed helper/controller changes after independently reproducing12cases7pass/5fail/cancelled0/naturalexit1. Original and fixture-correction provenance in20-54 investigation note.
- Product delta: controller one argument; helper optional lifecycle, cancellation hook and raced fetch+response.text, synchronous invalidation, transport abort where available, timer and identity-safe hook cleanup. Existing standalone caller and engine/player/native selection unchanged. Prettier formatting outside helper was restored to avoid engine edits. No existing tests modified.
- New unique tests/url-health-hls-lifecycle.test.mjs35scenarios; original isolated modules yield13pass/22fail/cancelled0 (baseline mutant). Fixed original12case diagnostic12pass/0fail/0cancelled/naturalexit0. No baseline failure erased.
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD node --test tests/url-health-hls-lifecycle.test.mjs tests/url-health-head-lifecycle.test.mjs tests/url-health-lifecycle.test.mjs tests/url-health-multisource.test.mjs tests/ui/url-health-controller.test.mjs tests/services/hlsPlayback.test.mjs > /tmp/client9-hls-focused-final.tap 2>&1`: approvedhost naturalexit0;6files/129pass/0fail/0cancelled/1042.269662ms. New35+existing94. Initial equivalent candidate with redundant controller preguard also129pass; removed redundant guard then verified final again.
- `python3 /tmp/client9-hls-mutants.py > /tmp/client9-hls-mutants-final.log 2>&1`: runnerexit0;19isolated module/cache variants each naturalexit1/fail>0/cancelled0. Results/tmp/client9-hls-mutants.json. Initial retainedtimer variant hit engineclearTimeout and survived; corrected exacthelper suffix, rejected30scenarios, reran final19 on final candidate. No worktree source mutated for experiments.
- `env -u NODE_TEST_CONTEXT -u UNIT_TEST_SHARD npm run lint > /tmp/client9-hls-lint-final.log 2>&1`: approvedhost naturalexit0/all11checks. Earlier candidate lint alsoexit0. `git diff --check` exit0. No lint baseline/config change.
- Full/normal gate, build/artifact and browser HLS lifecycle UNRUN for this follow-on; no product submission. Storage4 owns heavy slot. Await serialized slot after commit/currentunstable merge. No decoder/realplayback/network-byte/CPU/livepeer/realcloud/signer/production/wholeStage3 proof; torrent/RangeGET remain open.
+559
View File
@@ -0,0 +1,559 @@
import test from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import { probeHlsManifest } from "../js/services/hlsPlayback.js";
// 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, { ignoreAbort = false, missingAbort = false } = {}) {
const dom = new JSDOM("<!doctype html><body></body>", {
url: "https://example.invalid"
});
const names = [
"window",
"document",
"HTMLElement",
"localStorage",
"fetch",
"IntersectionObserver",
"requestAnimationFrame",
"setTimeout",
"clearTimeout",
"AbortController"
];
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
});
if (missingAbort) globalThis.AbortController = undefined;
const policy = getCardLivenessPolicy();
setCardLivenessPolicy("show-pending");
const resources = new Set(),
listeners = new Map(),
handlers = new Map(),
pauses = [];
const observers = [],
media = [],
heads = [],
promises = [],
warnings = [],
registrations = [],
visibility = [],
checks = [];
const id = (name) => `url-lifecycle-${++sequence}-${name}`;
const url = (name) => `https://media.invalid/${name}.m3u8`;
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
}))
);
}
};
const bodies = [],
clears = [];
const timers = new Map(),
nativeSetTimeout = globalThis.setTimeout,
nativeClearTimeout = globalThis.clearTimeout;
globalThis.setTimeout = (fn, ms) => {
if (!ms) return nativeSetTimeout(fn, ms);
const handle = {};
timers.set(handle, { fn, ms });
return handle;
};
globalThis.clearTimeout = (handle) => {
clears.push(handle);
if (!timers.delete(handle)) nativeClearTimeout(handle);
};
globalThis.fetch = (source, options) =>
options.method === "HEAD"
? Promise.resolve({ ok: true, status: 200, type: "basic" })
: new Promise((resolve, reject) => {
const head = { source, options, resolve, reject, aborted: 0 };
heads.push(head);
options.signal?.addEventListener(
"abort",
() => {
head.aborted++;
if (!ignoreAbort)
reject(new DOMException("Aborted", "AbortError"));
},
{ once: true }
);
});
const proto = dom.window.HTMLMediaElement.prototype;
const addListener = proto.addEventListener,
removeListener = proto.removeEventListener;
proto.addEventListener = function (type, fn, options) {
if (["loadeddata", "canplay", "error"].includes(type)) {
if (!listeners.has(this)) listeners.set(this, new Map());
if (!handlers.has(this)) handlers.set(this, new Map());
listeners.get(this).set(type, fn);
handlers.get(this).set(type, fn);
}
return addListener.call(this, type, fn, options);
};
proto.removeEventListener = function (type, fn, options) {
listeners.get(this)?.delete(type);
return removeListener.call(this, type, fn, options);
};
proto.load = function () {
if (this.getAttribute("src")) {
if (!media.includes(this)) media.push(this);
resources.add(this);
} else resources.delete(this);
};
proto.pause = function () {
pauses.push(this);
};
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"),
consumer = controller
} = {}
) {
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);
consumer.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.
for (const head of heads)
head.resolve({
ok: true,
status: 200,
type: "basic",
text: async () => "#EXTM3U\nsegment.ts"
});
for (const body of bodies) body.resolve("#EXTM3U\nsegment.ts");
await flush();
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,
bodies,
clears,
resources,
listeners,
handlers,
pauses,
controller,
media,
heads,
timers,
promises,
warnings,
visibility,
checks,
feed,
video,
done,
finishAll
};
}
// Actual observer/controller/cache/manifest helper; only IO, clock and viewport
// boundaries are controlled. No engine/decoder/public network is involved.
const inFlight = (v) =>
cache.getInFlightUrlProbe(v.id, v.url, { confirmPlayable: true });
const cached = (v) => cache.getCachedUrlHealth(v.id, v.url);
const manifest = {
ok: true,
text: async () => "#EXTM3U\n#EXTINF:2\nsegment.ts"
};
function tracking(promise) {
const result = { settled: false };
promise.then((value) => Object.assign(result, { settled: true, value }));
return result;
}
function bodyReader(f) {
let resolve, reject;
const promise = new Promise((yes, no) => {
resolve = yes;
reject = no;
});
const body = { promise, resolve, reject };
f.bodies.push(body);
return body;
}
async function start(f, { phase = "fetch", mirror = false } = {}) {
const v = f.video(phase);
if (mirror) v.sources = [{ url: `${v.url}?mirror` }];
const old = f.feed([v]);
old.observer.show();
await flush();
assert.equal(f.heads.length, 1);
assert.equal(f.heads[0].options.method, "GET");
assert.equal(f.media.length, 0);
let body;
if (phase === "body") {
body = bodyReader(f);
f.heads[0].resolve({ ok: true, text: () => body.promise });
await flush();
}
return {
v,
old,
body,
promise: f.promises[0],
result: tracking(f.promises[0])
};
}
for (const phase of ["fetch", "body"]) {
for (const transport of ["abortable", "ignores-abort", "missing-abort"]) {
test(`HLS ${phase} final owner releases resources with ${transport}`, async (t) => {
const f = fixture(t, {
ignoreAbort: transport === "ignores-abort",
missingAbort: transport === "missing-abort"
});
const { v, old, result } = await start(f, { phase, mirror: true });
const timer = [...f.timers.keys()][0];
old.dispose();
old.dispose();
await flush();
assert.equal(
result.settled,
true,
"settle independently of fetch/body cooperation"
);
assert.equal(result.value, null);
assert.equal(f.heads[0].aborted, transport === "missing-abort" ? 0 : 1);
assert.equal(f.timers.size, 0);
assert.equal(f.clears.filter((x) => x === timer).length, 1);
assert.equal(cached(v), null);
assert.equal(inFlight(v), null);
assert.equal(f.heads.length, 1, "no obsolete mirror");
assert.equal(f.media.length, 0);
assert.equal(old.badges[0].dataset.urlHealthState, "checking");
assert.equal(f.controller.activeVideoProbes, 0);
assert.equal(f.controller.videoProbeQueue.length, 0);
await f.finishAll();
assert.equal(cached(v), null, "late completion cannot cache");
assert.equal(f.clears.filter((x) => x === timer).length, 1);
});
}
for (const timing of ["same-turn", "after-flush"])
for (const late of ["success", "error", "timer"]) {
test(`HLS ${phase} ${timing} replacement survives old ${late}`, async (t) => {
const f = fixture(t, { ignoreAbort: true });
const { v, old, body, promise, result } = await start(f, { phase });
const staleTimer = [...f.timers.values()][0].fn;
old.dispose();
if (timing === "after-flush") await flush();
const live = f.feed([v]);
live.observer.show();
const replacement = inFlight(v);
assert.notEqual(
replacement,
promise,
"new owner requires fresh inflight identity"
);
await flush();
assert.equal(f.heads.length, 2);
assert.equal(result.settled, true);
assert.equal(result.value, null);
assert.equal(inFlight(v), replacement);
assert.equal(f.timers.size, 1);
if (late === "timer") staleTimer();
else if (phase === "body") {
if (late === "success") body.resolve("#EXTM3U\nold.ts");
else body.reject(Error("late body failure"));
} else if (late === "success") f.heads[0].resolve(manifest);
else f.heads[0].reject(Error("late fetch failure"));
await flush();
assert.equal(inFlight(v), replacement);
assert.equal(cached(v), null);
assert.equal(f.timers.size, 1);
assert.equal(old.badges[0].dataset.urlHealthState, "checking");
f.heads[1].resolve(manifest);
await replacement;
await flush();
assert.equal(cached(v).status, "healthy");
assert.equal(live.badges[0].dataset.urlHealthState, "healthy");
assert.equal(old.badges[0].dataset.urlHealthState, "checking");
assert.equal(inFlight(v), null);
assert.equal(f.timers.size, 0);
});
}
for (const kind of ["shared", "cross-controller", "legacy"]) {
test(`HLS ${phase} cancellation preserves ${kind} survivor`, async (t) => {
const f = fixture(t);
const { v, old, result } = await start(f, { phase });
const consumer =
kind === "cross-controller"
? new UrlHealthController({ state: cache, logger: { warn() {} } })
: f.controller;
let live;
if (kind === "legacy") {
const badge = document.createElement("span");
document.body.append(badge);
consumer.handleUrlHealthBadge({ video: v, url: v.url, badgeEl: badge });
live = { badges: [badge] };
} else {
live = f.feed([v], { consumer });
live.observer.show();
}
await flush();
old.dispose();
await flush();
assert.equal(f.heads.length, 1);
assert.equal(f.heads[0].aborted, 0);
assert.equal(result.settled, false);
assert.equal(f.timers.size, 1);
await f.finishAll();
assert.equal(result.value.status, "healthy");
assert.equal(cached(v).status, "healthy");
assert.equal(live.badges[0].dataset.urlHealthState, "healthy");
assert.equal(old.badges[0].dataset.urlHealthState, "checking");
assert.equal(f.timers.size, 0);
});
}
}
for (const outcome of ["success", "error", "bad-body", "timeout"]) {
test(`HLS live ${outcome} preserves health/cache semantics`, async (t) => {
const f = fixture(t);
const { v, old, result } = await start(f);
if (outcome === "timeout") [...f.timers.values()][0].fn();
else
f.heads[0].resolve(
outcome === "success"
? manifest
: outcome === "error"
? { ok: false, status: 500 }
: { ok: true, text: async () => "<html>not a manifest</html>" }
);
await flush();
assert.equal(result.settled, true);
const status =
outcome === "success"
? "healthy"
: outcome === "timeout"
? "timeout"
: "offline";
assert.equal(result.value.status, status);
assert.equal(cached(v).status, status);
assert.equal(old.badges[0].dataset.urlHealthState, status);
assert.equal(f.timers.size, 0);
assert.equal(f.heads[0].aborted, outcome === "timeout" ? 1 : 0);
});
}
test("HLS disposal during HEAD handoff prevents manifest GET", async (t) => {
const f = fixture(t);
const v = f.video("handoff"),
old = f.feed([v]);
old.observer.show();
old.dispose();
await flush();
const result = tracking(f.promises[0]);
await flush();
assert.equal(result.settled, true);
assert.equal(result.value, null);
assert.equal(f.heads.length, 0);
assert.equal(f.timers.size, 0);
assert.equal(cached(v), null);
});
test("HLS native preference keeps cancellable media path", async (t) => {
const f = fixture(t);
f.dom.window.HTMLMediaElement.prototype.canPlayType = () => "probably";
const v = f.video("native"),
old = f.feed([v]);
old.observer.show();
await flush();
assert.equal(f.heads.length, 0);
assert.equal(f.media.length, 1);
assert.equal(f.controller.activeVideoProbes, 1);
old.dispose();
await flush();
const result = tracking(f.promises[0]);
await flush();
assert.equal(result.settled, true);
assert.equal(result.value, null);
assert.equal(f.controller.activeVideoProbes, 0);
assert.equal(f.timers.size, 0);
assert.equal(f.resources.size, 0);
assert.equal(cached(v), null);
});
for (const preCancelled of [false, true])
test(`manifest helper skips cancelled lifecycle before start ${preCancelled}`, async (t) => {
const f = fixture(t);
const lifecycle = {
owners: new Set(preCancelled ? [{}] : []),
cancelled: preCancelled
};
const result = tracking(
probeHlsManifest(f.video("prestart").url, 5000, lifecycle)
);
await flush();
assert.equal(
result.settled,
true,
"pre-cancelled helper settles without IO"
);
assert.equal(result.value, "cancelled");
assert.equal(lifecycle.cancelled, true);
assert.equal(f.heads.length, 0);
assert.equal(f.timers.size, 0);
});
test("manifest helper clears its hook exactly once on cancellation", async (t) => {
const f = fixture(t, { ignoreAbort: true });
const lifecycle = { owners: new Set([{}]) };
const promise = probeHlsManifest(f.video("hook").url, 5000, lifecycle);
const cancel = lifecycle.cancelActive;
const timer = [...f.timers.keys()][0];
cancel();
cancel();
assert.equal(lifecycle.cancelled, true);
const result = tracking(promise);
await flush();
assert.equal(
result.settled,
true,
"cancelled helper settles without cooperative IO"
);
assert.equal(result.value, "cancelled");
assert.equal(lifecycle.cancelActive, null);
assert.equal(f.heads[0].aborted, 1);
assert.equal(f.clears.filter((x) => x === timer).length, 1);
assert.equal(f.timers.size, 0);
});
test("manifest helper finalizer preserves a successor cancellation hook", async (t) => {
const f = fixture(t);
const lifecycle = { owners: new Set([{}]) };
const promise = probeHlsManifest(f.video("successor").url, 5000, lifecycle);
const successor = () => {};
lifecycle.cancelActive = successor;
f.heads[0].resolve(manifest);
assert.equal(await promise, "ok");
assert.equal(lifecycle.cancelActive, successor);
assert.equal(f.timers.size, 0);
});
test("HLS cancellation during native-support check prevents manifest start", async (t) => {
const f = fixture(t);
const v = f.video("support-handoff"),
old = f.feed([v]);
f.dom.window.HTMLMediaElement.prototype.canPlayType = () => {
old.dispose();
return "";
};
old.observer.show();
await flush();
const result = tracking(f.promises[0]);
await flush();
assert.equal(result.settled, true);
assert.equal(result.value, null);
assert.equal(f.heads.length, 0);
assert.equal(f.timers.size, 0);
assert.equal(cached(v), null);
});
@@ -0,0 +1,5 @@
- [x] Preserve original actual12mode baseline5fail/7pass and fixture corrections.
- [x] Minimal optional HLS cancellation plumbing and35 new lifecycle/helper scenarios.
- [x] Focused129/6files and19 failing isolated mutants,cancelled0.
- [ ] Final lint11,commit/merge currentunstable and READY/resource request.
- [ ] Allocated full/normal landing/build/nativebrowser; no wider HLSplayback/torrent/RangeGET/Stage3 claim.