client-reliability-3: cancel queued torrent probes after last card owner disposes

This commit is contained in:
thePR0M3TH3AN committed 2026-10-19 17:56:25 -04:00
1 parent 66832dc3a9
commit 712b05cf87
9 files changed
+626 -45

No files matched your search

+68
View File
@@ -663,3 +663,71 @@ test_integrity_note:
Original concurrent-promise, pool-ready and post-init no-throw assertions remain. The old empty connection result accidentally modeled failure; init correctly scheduled manager-owned retries that bypassed its client-level stub. The fixture now models successful init at the manager boundary and releases its feed subscription. This is harness cleanup, not a Nostr product change.
Mutants: a test-local historical early-return init variant fails the pending-barrier assertion (exit1, 1 failed); a connection-failure fixture variant fails the no-retry assertion (exit1, 1 failed). Both exit naturally and are reverted. No product file was edited for either mutant.
---
# client-reliability-3
```yaml
test_integrity_note:
change_type: ["new_tests"]
scenarios:
- id: SCN-last-probe-owner-disposal
given: "five real DOM cards register through actual gridHealth/cardObserver; four probes are active and one queued"
when: "the feed is disposed/removed and active probes finish"
then: "zero obsolete probes start"
- id: SCN-shared-queued-owner
given: "two cards share one queued torrent source"
when: "one registration disposes while its DOM remains connected"
then: "the live owner still probes and becomes healthy; disposed owner receives no stale UI/callback"
- id: SCN-cancelled-replacement-inflight
given: "last owner cancels queued work then a replacement requests the same source before old settlement"
when: "old completion drains and another owner joins before a slot frees"
then: "no unhealthy cancellation cache is written, only one new probe starts, and both replacements become healthy/cacheable"
- id: SCN-probe-priority-concurrency
given: "four active probes, queued mixed priorities and a cancelled source"
when: "slots free one at a time"
then: "four-slot maximum and stable priority order remain; cancelled work never starts"
- id: SCN-hidden-healthy-reveal
given: "a hide-all card is hidden pending liveness"
when: "registration eagerly probes and returns healthy"
then: "raw magnet/webseed bytes reach probe IO and the card reveals without an intersection"
- id: SCN-active-shared-late-entry
given: "an active source loses its owner, receives a late observer entry and gains a live owner"
when: "the unchanged active probe completes"
then: "no duplicate starts or disposed result writes; live owner becomes healthy"
- id: SCN-repeat-disposal-reattach
given: "a disposed container has a new queued registration"
when: "the old disposer is called again"
then: "new owner survives and becomes healthy"
- id: SCN-ownerless-active-cache
given: "an already active probe has no remaining owner"
when: "it finishes before a later live card requests that source"
then: "the active result remains cached; no new probe or stale UI write occurs"
- id: SCN-existing-probe-cache-ttl
given: "completed healthy and negative results"
when: "cards request before, at, and after the existing TTL boundary"
then: "both cached outcomes reuse until the existing inclusive TTL expires, then both probe again"
observable_outcomes:
- "actual probePeers start count/order, arguments and maximum active count"
- "real DOM badge health/hidden state, observer disposal and callback counts"
- "storage cache absence for cancellation and reuse/expiry for completed probes"
determinism_controls:
- "actual gridHealth/cardObserver and DOM, controllable observer and deferred probePeers IO"
- "explicit viewport geometry, unique sources and isolated storage; fake Date.now only for TTL scenario"
- "task-turn promise draining without wall-clock sleeps, polling or retries"
- "native Chromium/Firefox observer smoke against built dist, external traffic blocked"
anti_cheat_rationale:
prevents:
- "unconditional queue cancellation: shared-owner mutant fails"
- "no-op cancellation, stale inflight deletion or cancelled cache poisoning"
- "changed concurrency/priority or hidden eager path"
- "stale result/listener/observer effects and repeated old disposal"
- "cancelling or dropping cache from already active work"
- "timeout/retry/sleep masking or hard-coded healthy results"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
Grouped mutation coverage: last-owner cancellation mutant kills SCN-last-probe-owner-disposal; unconditional cancel kills SCN-shared-queued-owner; unconditional inflight deletion and cancelled-normalization mutants each kill SCN-cancelled-replacement-inflight; reversed queue priority kills SCN-probe-priority-concurrency; disabled eager path kills SCN-hidden-healthy-reveal; stale result guard and late-entry membership mutants each kill SCN-active-shared-late-entry; non-idempotent disposer kills SCN-repeat-disposal-reattach; owner-dependent active caching kills SCN-ownerless-active-cache; missing cache writes kills SCN-existing-probe-cache-ttl. All11 variants exit1 with1 failed scenario,0 cancelled, and are reverted. Browser checks corroborate the same disposal/shared-owner/hidden-reveal/source-visibility scenarios with native observers; no browser CPU or actual media fallback claim.
@@ -0,0 +1,4 @@
- Goal: cancel unstarted torrent health probes when last feed/card owner unregisters, preserving live shared owners and active probes.
- Scope: gridHealth.js, optional queue helper and focused tests/integrity/area notes. Excludes app, URL health, webtorrent implementation, playback/magnet helpers, cache TTLs/concurrency, runner/kit. Dispatch completion mailbox required; no scheduler.
- Revalidated unchanged gridHealth/cardObserver from filedAtf9adfc8f through startup66832dc3. Adapted actual dispatch five-card reproduction to own imports:4 start,1 queues, dispose/remove then release ->1 obsolete start; assertion exit1.
- Plan: introduce cancellable pending queue records and per-card leases; release at existing onCardUnregister; preserve active completion and cache, identity-check inflight deletion; reject stale observer/UI effects; scenario tests for shared owners, cancellation/re-request race, priority/concurrency, hidden reveal; grouped mutants; focused/full60s-file/lint/build/browser; commit/merge/normal landq/ancestry then close and report.
@@ -0,0 +1,9 @@
# client-reliability-3: queued probe ownership
- Actual baseline at66832dc3 (source unchanged from filedAtf9adfc8f): actual attachHealthBadges + DOM + observer, four deferred probePeers calls active and fifth queued; disposing/removing feed then releasing active work starts one obsolete probe (expected0), assertion exit1.
- Queue records own a Set of live card requests. onCardUnregister releases the matching request; only an unstarted job with no remaining owner can cancel. Cancellation resolves null, skips normalization/cache, and removes inflight synchronously. Promise finalization checks record identity so an old cancellation cannot delete replacement inflight work.
- Active jobs are never aborted or removed from inflight on disposal; their normal cache result remains usable. Existing raw magnets/webseeds, four-slot concurrency, stable priority ordering, healthy/negative TTL remain unchanged.
- Live-card membership rejects late observer entries. Each request also guards result writes by ownership/identity even while disposed DOM remains connected. Per-registration badge listener removal and idempotent disposer prevent stale callbacks or an old disposer unregistering a newly attached feed.
- Existing cardObserver unregister hook is sufficient; no shared helper, URL health, WebTorrent implementation, app, cache policy, concurrency, styling, runner or kit edits.
- Browser smoke uses actual built modules/native observers and fake external probe IO only; both Chromium and Firefox pass. No external request attempted. Hosted-only/torrent-only source visibility checked; real playback fallback, live torrent peers, CPU/RSS and active abort behavior unmeasured/out of scope.
- Fixture correction before acceptance: use explicit viewport geometry so observer priorities are deterministic; initial unpinned JSDOM viewport reversed two fixture assumptions. No existing assertion changed. Eleven mutants independently kill the nine grouped lifecycle scenarios (details in integrity note).
+2
View File
@@ -14,3 +14,5 @@
- 2026-10-19: client-reliability-1 and client-reliability-2 ready for normal landing at181eae95: profile27/27 and race1/1 natural exit; all425 unit files passed with60s/file bound; lint11/11. Four deliberately broken variants fail and are reverted. Dispatch routing unblocked the second item; no product Nostr/shared-runner changes. Remaining: durable normal queue landing, unstable verification, then backlog close. Browser/build and real IO unverified (test-only scope). See test_logs/TEST_LOG_2026-10-19_20-35-00_client-reliability-both.md.
- 2026-10-19: client-reliability-1 and client-reliability-2 finished: durable normal-gate landing de6918ce (submission683cedf0), gate exit0 and unstable ancestry verified. Profile27/27, race1/1 natural exit, lint11/11, full425/425 file exits0; all four mutants rejected. Backlog closed after landing. No unfinished product work for these items; browser/build/real IO unverified (test-only scope). Closure metadata queued separately; see test_logs/TEST_LOG_2026-10-19_20-34-50_client-reliability-landed.md.
- 2026-10-19: client-reliability-3 implemented at current worktree: actual five-card obsolete starts1→0 after last-owner disposal; shared queued/live owners and active probe cache preserved; inflight identity and stale registration guards. Focused25/25 across4 files,11 mutants rejected, lint11/11, build/artifact verification pass, native Chromium/Firefox built-dist lifecycle smoke pass with0 external requests/page errors. Remaining: committed merged-tree full426-file gate, normal landq, durable ancestry/close and dispatch events. Active abort, URL probes, actual media fallback/peers and CPU/RSS unverified/out of scope; entire Stage3 performance plan is not complete.
@@ -1,7 +1,7 @@
{
"id": "client-reliability-3",
"area": "client-reliability",
"status": "open",
"status": "in-progress",
"title": "Cancel queued torrent health probes when their last feed owner is disposed",
"blockedOn": null,
"addedOn": "2026-10-19",
+110 -44
View File
@@ -183,41 +183,47 @@ function normalizeResult(result) {
};
}
function queueProbe(magnet, cacheKey, priority = 0, webSeeds = []) {
function queueProbe(magnet, cacheKey, priority = 0, webSeeds = [], owner) {
if (!magnet) {
return Promise.resolve(null);
return { promise: Promise.resolve(null), owners: new Set() };
}
const cached = getCacheEntry(cacheKey);
if (cached) {
return Promise.resolve(cached);
return { promise: Promise.resolve(cached), owners: new Set() };
}
if (probeInflight.has(cacheKey)) {
return probeInflight.get(cacheKey);
const record = probeInflight.get(cacheKey);
record.owners.add(owner);
return record;
}
const job = probeQueue
.run(
() =>
torrentClient
.probePeers(magnet, {
timeoutMs: PROBE_TIMEOUT_MS,
maxWebConns: 2,
polls: PROBE_POLL_COUNT,
urlList: webSeeds,
})
.catch((err) => ({
healthy: false,
peers: 0,
reason: "error",
error: err,
appendedTrackers: false,
hasProbeTrackers: false,
})),
priority
)
const record = { owners: new Set([owner]), cancelled: false };
record.job = probeQueue.run(
() =>
torrentClient
.probePeers(magnet, {
timeoutMs: PROBE_TIMEOUT_MS,
maxWebConns: 2,
polls: PROBE_POLL_COUNT,
urlList: webSeeds,
})
.catch((err) => ({
healthy: false,
peers: 0,
reason: "error",
error: err,
appendedTrackers: false,
hasProbeTrackers: false,
})),
priority
);
record.promise = record.job.promise
.then((result) => {
if (record.cancelled) {
return null;
}
const normalized = normalizeResult(result);
if (cacheKey) {
setCacheEntry(cacheKey, normalized);
@@ -225,16 +231,29 @@ function queueProbe(magnet, cacheKey, priority = 0, webSeeds = []) {
return normalized;
})
.finally(() => {
if (cacheKey) {
if (cacheKey && probeInflight.get(cacheKey) === record) {
probeInflight.delete(cacheKey);
}
});
if (cacheKey) {
probeInflight.set(cacheKey, job);
probeInflight.set(cacheKey, record);
}
return job;
return record;
}
function releaseProbe(record, owner, cacheKey) {
owner.active = false;
record.owners.delete(owner);
// A running probe keeps its existing lifecycle and cache behavior. Only work
// still waiting for a slot can be cancelled when its last card unregisters.
if (!record.owners.size && record.job?.cancel()) {
record.cancelled = true;
if (probeInflight.get(cacheKey) === record) {
probeInflight.delete(cacheKey);
}
}
}
const PROBE_CACHE_TTL_MS = FIVE_MINUTES_MS;
@@ -255,9 +274,10 @@ class ProbeQueue {
}
run(task, priority = 0) {
return new Promise((resolve, reject) => {
let job;
const promise = new Promise((resolve, reject) => {
const normalizedPriority = Number.isFinite(priority) ? priority : 0;
const job = {
job = {
task,
resolve,
reject,
@@ -266,6 +286,18 @@ class ProbeQueue {
this.enqueue(job);
this.drain();
});
return {
promise,
cancel: () => {
const index = this.queue.indexOf(job);
if (index < 0) {
return false;
}
this.queue.splice(index, 1);
job.resolve(null);
return true;
},
};
}
enqueue(job) {
@@ -432,10 +464,13 @@ function setBadge(card, state, details) {
card,
state: normalizedState,
peers: hasPeerCount ? peersValue : null,
reason: details && typeof details.reason === "string" ? details.reason : null,
reason:
details && typeof details.reason === "string" ? details.reason : null,
webseedOnly,
checkedAt:
details && Number.isFinite(details.checkedAt) ? Number(details.checkedAt) : null,
details && Number.isFinite(details.checkedAt)
? Number(details.checkedAt)
: null,
text: badge.textContent,
tooltip,
role: entry.role,
@@ -473,8 +508,8 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) {
const cachedState = cached.healthy
? "healthy"
: cached.hasProbeTrackers
? "unhealthy"
: "unknown";
? "unhealthy"
: "unknown";
setBadge(card, cachedState, cached);
return;
}
@@ -485,13 +520,19 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) {
const webSeed = encodedUrl ? decodeURIComponent(encodedUrl) : "";
const webSeeds = webSeed ? [webSeed] : [];
const probePromise = queueProbe(magnet, infoHash, priority, webSeeds);
const owner = { active: true };
const record = queueProbe(magnet, infoHash, priority, webSeeds, owner);
owner.release = () => releaseProbe(record, owner, infoHash);
pendingByCard.set(card, probePromise);
pendingByCard.set(card, owner);
probePromise
record.promise
.then((result) => {
if (!owner.active || pendingByCard.get(card) !== owner) {
return;
}
pendingByCard.delete(card);
record.owners.delete(owner);
if (!card.isConnected) {
return;
}
@@ -510,11 +551,15 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) {
setBadge(card, "unhealthy", result);
})
.catch((err) => {
userLogger.warn("[gridHealth] probe failed", err);
if (!owner.active || pendingByCard.get(card) !== owner) {
return;
}
pendingByCard.delete(card);
record.owners.delete(owner);
if (!card.isConnected) {
return;
}
userLogger.warn("[gridHealth] probe failed", err);
setBadge(card, "unhealthy");
});
}
@@ -522,7 +567,10 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) {
const gridCardObserver = createCardObserver({
rootMargin: ROOT_MARGIN,
threshold: 0.01,
createState: () => ({ pendingByCard: new WeakMap() }),
createState: () => ({
pendingByCard: new WeakMap(),
liveCards: new WeakSet(),
}),
prepareEntries: (entries) => {
const viewportCenter = getViewportCenter();
return prioritizeEntries(entries, viewportCenter);
@@ -532,10 +580,12 @@ const gridCardObserver = 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;
},
onCardRegister: ({ card, state }) => {
state.liveCards.add(card);
if (!card.dataset.magnet) {
setBadge(card, "unhealthy", { reason: "missing-source" });
return;
@@ -555,8 +605,16 @@ const gridCardObserver = createCardObserver({
});
}
},
onCardUnregister: ({ card, state }) => {
state.liveCards.delete(card);
const owner = state.pendingByCard.get(card);
if (owner) {
owner.release();
state.pendingByCard.delete(card);
}
},
onCardVisible: ({ card, meta, state }) => {
if (!state || !(state.pendingByCard instanceof WeakMap)) {
if (!state?.liveCards.has(card)) {
return;
}
const priority =
@@ -575,9 +633,17 @@ export function attachHealthBadges(container, options = {}) {
if (!(container instanceof HTMLElement)) {
return () => {};
}
if (options && typeof options.onUpdate === "function") {
badgeUpdateListeners.add(options.onUpdate);
}
const listener =
typeof options?.onUpdate === "function"
? (payload) => options.onUpdate(payload)
: null;
if (listener) badgeUpdateListeners.add(listener);
gridCardObserver.observe(container);
return () => gridCardObserver.disconnect(container);
let disposed = false;
return () => {
if (disposed) return;
disposed = true;
if (listener) badgeUpdateListeners.delete(listener);
gridCardObserver.disconnect(container);
};
}
@@ -0,0 +1,14 @@
# client-reliability-3 pre-commit evidence (startup66832dc3)
- Adapted /tmp/bitvid-dispatch-obsolete-probe-scenario.mjs imports to this worktree. Baseline: four initial active probes, observer disconnected,1 obsolete start after feed disposal/removal, exit1 assertion1!==0. Source unchanged since filedAtf9adfc8f.
- Fixed same actual badge/observer scenario: four initial active,0 obsolete starts, exit0. No live torrent traffic; only probePeers boundary deferred.
- Initial focused draft:5/7 pass;2 fixture expectations exposed default viewport ordering. Explicit viewport geometry controls priority; expectations unchanged. Direct final focused logs record9 lifecycle and6 priority scenarios, all exit0 naturally; related source visibility and card observer suites also exit0 (counts below).
- Intermediate node --test invocation reported two file wrappers, not scenario totals; use direct per-file readable reporters for authoritative scenario counts.
- Eleven mutations: cancel, shared-owner, inflight identity, priority, hidden eager path, stale result, TTL/cache write, repeated disposer, late observer entry, cancelled-cache normalization, ownerless-active cache. Each selected scenario exits1 naturally,1 failed/0 passed/cancelled. Restored source after all variants; no product files outside gridHealth.js touched.
- `npm run lint` unrestricted: exit0,11 stages. Sandbox git subprocess restrictions are known; no gate change.
- `npm run build`: exit0 including deployment artifact verification. Existing Browserslist data-age warning; dependency update not attempted.
- Initial browser smoke sandbox attempt: exit1, loopback listen EPERM; authorized unrestricted smoke exits0, closes its ephemeral server/browsers, does not touch port3000.
- Chromium and Firefox, built dist modules with native observers and virtual probe IO: each4 active/0 obsolete starts, live shared owner healthy/disposed owner checking/0 stale updates, hidden eager probe and reveal, torrent-only and hosted-only source visibility true,0 page errors and0 attempted external requests. Script /tmp/client-reliability-3-browser.mjs, log /tmp/client-reliability-3-browser-unrestricted.log.
- GitHub overlap API check: exit6 DNS unavailable; no PR created and dispatch reserves this scope.
- No CPU/RSS benefit or real media/torrent fallback claimed. Active probe cancellation, live peers, URL checks, signer/wallet/production/deployment unverified/out of scope. Full merged-tree unit/landing gates follow.
- Final direct per-file focused runs: lifecycle9/9 (225.86ms), existing priority6/6 (136.30ms), source visibility9/9 (211.62ms), observer disposal1/1 (135.19ms); all exit0 naturally,0 failed/cancelled/skipped. Total25 scenarios across4 files. Final source restored after all mutants.
+412
View File
@@ -0,0 +1,412 @@
import "./test-helpers/setup-localstorage.mjs";
import test from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import { torrentClient } from "../js/webtorrent.js";
import { attachHealthBadges } from "../js/gridHealth.js";
import {
FIVE_MINUTES_MS,
setCardLivenessPolicy,
getCardLivenessPolicy
} from "../js/constants.js";
import { readTorrentProbeFromStorage } from "../js/utils/storage.js";
const healthy = {
healthy: true,
peers: 1,
reason: "peer",
hasProbeTrackers: true
};
let nextHash = 1;
const magnet = () =>
"magnet:?xt=urn:btih:" + (nextHash++).toString(16).padStart(40, "0");
// A task-turn boundary drains the queue's promise continuations and badge writes;
// no wall-clock delay, polling or live network is involved.
const flush = () => new Promise((resolve) => setImmediate(resolve));
function fixture(t) {
const dom = new JSDOM("<!doctype html><body></body>", {
url: "https://example.invalid"
});
Object.defineProperty(dom.window, "innerHeight", { value: 20 });
const previousPolicy = getCardLivenessPolicy();
const saved = new Map(
[
"window",
"document",
"HTMLElement",
"localStorage",
"IntersectionObserver",
"__BITVID_CARD_LIVENESS_POLICY__"
].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 observers = [];
class Observer {
constructor(callback) {
this.callback = callback;
this.cards = [];
observers.push(this);
}
observe(card) {
this.cards.push(card);
}
takeRecords() {
return [];
}
disconnect() {
this.disconnected = true;
}
show() {
// Intentionally allow late delivery after disconnect to test stale entries.
this.callback(
this.cards.map((target, index) => ({
target,
isIntersecting: true,
intersectionRatio: 1,
intersectionRect: { top: index * 20, width: 100, height: 20 }
}))
);
}
}
globalThis.IntersectionObserver = Observer;
const calls = [];
const disposals = [];
let active = 0;
let peakActive = 0;
t.mock.method(torrentClient, "probePeers", (source, options) => {
active += 1;
peakActive = Math.max(peakActive, active);
let resolve;
const promise = new Promise((done) => {
resolve = done;
});
const call = {
source,
options,
settled: false,
finish(result = healthy) {
if (call.settled) return;
call.settled = true;
active -= 1;
resolve(result);
}
};
calls.push(call);
return promise;
});
setCardLivenessPolicy("show-pending");
t.after(async () => {
disposals.forEach((dispose) => dispose());
// Disposing removes queued owners; only the controlled active calls remain.
calls.forEach((call) => call.finish());
await flush();
setCardLivenessPolicy(previousPolicy);
dom.window.close();
for (const [key, descriptor] of saved) {
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
else delete globalThis[key];
}
});
function feed(sources, options = {}) {
const container = document.createElement("section");
const cards = sources.map((source) => {
const card = document.createElement("article");
card.className = "card";
card.dataset.videoId = source;
card.dataset.magnet = source;
if (options.hidden) {
card.hidden = true;
card.dataset.urlHealthUrl = encodeURIComponent(
"https://media.example.invalid/video.mp4"
);
card.dataset.urlHealthState = "checking";
}
const badge = document.createElement("span");
badge.className = "torrent-health-badge";
card.appendChild(badge);
container.appendChild(card);
return card;
});
document.body.appendChild(container);
const dispose = attachHealthBadges(container, options);
disposals.push(dispose);
return { container, cards, dispose, observer: observers.at(-1) };
}
function saturate() {
const blockers = feed(Array.from({ length: 4 }, magnet));
blockers.observer.show();
assert.equal(
calls.length,
4,
"current four-probe concurrency must be saturated"
);
return blockers;
}
function reattach(feed) {
const dispose = attachHealthBadges(feed.container);
disposals.push(dispose);
return { ...feed, dispose, observer: observers.at(-1) };
}
return {
feed,
reattach,
saturate,
calls,
get peakActive() {
return peakActive;
}
};
}
test("last feed owner disposal prevents the fifth queued probe from starting", async (t) => {
const f = fixture(t);
const feed = f.feed(Array.from({ length: 5 }, magnet));
feed.observer.show();
assert.equal(f.calls.length, 4);
feed.dispose();
feed.container.remove();
assert.equal(feed.observer.disconnected, true);
f.calls.forEach((call) => call.finish());
await flush();
assert.equal(f.calls.length, 4, "obsolete queued source must never start");
});
test("disposing one of two queued source owners preserves the live owner", async (t) => {
const f = fixture(t);
f.saturate();
const source = magnet();
const disposedUpdates = [];
const first = f.feed([source], {
onUpdate: (event) => disposedUpdates.push(event.state)
});
const second = f.feed([source]);
first.observer.show();
second.observer.show();
assert.equal(f.calls.length, 4);
first.dispose(); // Keep DOM connected: stale writes must be guarded by ownership.
disposedUpdates.length = 0;
f.calls[0].finish();
await flush();
assert.equal(
f.calls.length,
5,
"remaining live owner must retain the shared queued probe"
);
assert.equal(f.calls[4].source, source);
f.calls[4].finish();
await flush();
assert.equal(second.cards[0].dataset.streamHealthState, "healthy");
assert.equal(first.cards[0].dataset.streamHealthState, "checking");
assert.deepEqual(
disposedUpdates,
[],
"disposed registration must receive no later badge notifications"
);
});
test("cancelled source can be re-requested before old settlement without cache poison or duplicate work", async (t) => {
const f = fixture(t);
f.saturate();
const source = magnet();
const first = f.feed([source]);
first.observer.show();
first.dispose();
const replacement = f.feed([source]);
replacement.observer.show(); // New inflight record before cancelled promise settles.
await flush();
assert.equal(
readTorrentProbeFromStorage(source.slice(-40)),
null,
"cancelled job must not cache an unhealthy result"
);
assert.equal(replacement.cards[0].dataset.streamHealthState, "checking");
const sharedReplacement = f.feed([source]);
sharedReplacement.observer.show(); // Must still find the replacement inflight record.
f.calls.forEach((call) => call.finish());
await flush();
assert.equal(
f.calls.filter((call) => call.source === source).length,
1,
"old finalizer must not delete newer inflight work"
);
f.calls.at(-1).finish();
await flush();
for (const feed of [replacement, sharedReplacement]) {
assert.equal(feed.cards[0].dataset.streamHealthState, "healthy");
}
const cached = f.feed([source]);
cached.observer.show();
assert.equal(cached.cards[0].dataset.streamHealthState, "healthy");
assert.equal(
f.calls.length,
5,
"successful replacement must retain normal cache reuse"
);
});
test("queue cancellation preserves stable priority and concurrency of four", async (t) => {
const f = fixture(t);
f.saturate();
const [first, low, high, cancelled] = Array.from({ length: 4 }, magnet);
const batch = f.feed([first, low]);
batch.observer.show(); // First gets baseline, low gets baseline minus one.
const newer = f.feed([high]);
newer.observer.show(); // Baseline: follows the earlier equal-priority first.
const obsolete = f.feed([cancelled]);
obsolete.observer.show();
obsolete.dispose();
for (const [slot, expected] of [
[0, first],
[4, high],
[5, low]
]) {
f.calls[slot].finish();
await flush();
assert.equal(f.calls.at(-1).source, expected);
}
assert.equal(
f.peakActive,
4,
"cancellation must not free active slots prematurely"
);
assert.deepEqual(
f.calls.slice(4).map((call) => call.source),
[first, high, low]
);
assert.equal(
f.calls.some((call) => call.source === cancelled),
false
);
});
test("healthy hidden card probes eagerly and reveals without an intersection", async (t) => {
const f = fixture(t);
setCardLivenessPolicy("hide-all");
const source = magnet() + "&tr=wss%3A%2F%2Ftracker.example.invalid";
const feed = f.feed([source], { hidden: true });
assert.equal(
f.calls.length,
1,
"hidden card must eagerly probe on registration"
);
assert.equal(feed.cards[0].hidden, true);
assert.deepEqual(f.calls[0].options.urlList, [
"https://media.example.invalid/video.mp4"
]);
assert.equal(
f.calls[0].source,
source,
"magnet bytes must reach the probe unchanged"
);
f.calls[0].finish();
await flush();
assert.equal(feed.cards[0].dataset.streamHealthState, "healthy");
assert.equal(feed.cards[0].hidden, false);
assert.equal(feed.cards[0].dataset.sourceVisibility, "visible");
});
test("active probe remains shared while disposed owners and late observer entries stay inert", async (t) => {
const f = fixture(t);
const source = magnet();
const first = f.feed([source]);
first.observer.show();
first.dispose();
first.observer.show(); // An already-delivered entry may arrive after disconnect.
const live = f.feed([source]);
live.observer.show();
assert.equal(
f.calls.length,
1,
"already active probe must remain deduplicated"
);
f.calls[0].finish();
await flush();
assert.equal(first.cards[0].dataset.streamHealthState, "checking");
assert.equal(live.cards[0].dataset.streamHealthState, "healthy");
});
test("repeated old disposal cannot cancel a reattached live registration", async (t) => {
const f = fixture(t);
f.saturate();
const source = magnet();
const old = f.feed([source]);
old.observer.show();
old.dispose();
const renewed = f.reattach(old);
renewed.observer.show();
old.dispose();
f.calls[0].finish();
await flush();
assert.equal(
f.calls.length,
5,
"old disposer must not unregister the new queued owner"
);
f.calls[4].finish();
await flush();
assert.equal(renewed.cards[0].dataset.streamHealthState, "healthy");
});
test("disposed active completion remains cached for a later live owner", async (t) => {
const f = fixture(t);
const source = magnet();
const old = f.feed([source]);
old.observer.show();
old.dispose();
f.calls[0].finish();
await flush();
assert.equal(old.cards[0].dataset.streamHealthState, "checking");
const live = f.feed([source]);
live.observer.show();
assert.equal(
f.calls.length,
1,
"ownerless active completion must retain its healthy cache result"
);
assert.equal(live.cards[0].dataset.streamHealthState, "healthy");
});
test("completed healthy and negative probes retain existing cache TTL behavior", async (t) => {
const f = fixture(t);
let time = 1_000_000;
t.mock.method(Date, "now", () => time);
const sources = [magnet(), magnet()];
const first = f.feed(sources);
first.observer.show();
f.calls[0].finish();
f.calls[1].finish({
healthy: false,
peers: 0,
hasProbeTrackers: true,
reason: "timeout"
});
await flush();
const cached = f.feed(sources);
cached.observer.show();
assert.deepEqual(
cached.cards.map((card) => card.dataset.streamHealthState),
["healthy", "unhealthy"]
);
assert.equal(f.calls.length, 2);
time += FIVE_MINUTES_MS;
cached.observer.show();
assert.equal(
f.calls.length,
2,
"existing inclusive TTL boundary must remain cached"
);
time += 1;
const expired = f.feed(sources);
expired.observer.show();
assert.equal(
f.calls.length,
4,
"both healthy and negative entries must expire under the existing TTL"
);
});
@@ -0,0 +1,6 @@
- [x] Rules/mailbox/claim/revalidate; actual five-card baseline reproduces1 obsolete start.
- [x] Last-owner queued cancellation, shared owners, cancellation settlement/cache/inflight identity; active unchanged and stale registrations inert.
- [x] Nine focused lifecycle scenarios; six existing priority scenarios; related source visibility/observer suites. Eleven deliberately broken variants fail and are reverted.
- [x] Lint11/11, build/artifact verification; Chromium/Firefox built-dist/native-observer local smoke0 obsolete starts/0 stale updates/hidden reveal/source health visibility.
- [ ] Commit, merge unstable, full npm test60s/file and final lint; normal landing, durable status/ancestry, dispatch landing event, close, closure records and final mailbox event.
- Not verified: real media fallback or peers, active abort, CPU/RSS, URL checks, owner preview refresh/deploy. Dispatch owns preview rebuild.