client-reliability-3: preserve shared badge callbacks and guard reentrant disposal

This commit is contained in:
thePR0M3TH3AN committed 2026-10-19 18:02:46 -04:00
1 parent 712b05cf87
commit 607d5ce1a1
6 files changed
+134 -9

No files matched your search

+29
View File
@@ -731,3 +731,32 @@ test_integrity_note:
```
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.
Additional client-reliability-3 scenarios (same invariant and controls):
```yaml
test_integrity_note:
change_type: ["new_tests"]
scenarios:
- id: SCN-shared-badge-listener-lifetime
given: "two feed registrations share the app callback"
when: "badge updates occur and registrations dispose one at a time"
then: "each event notifies once, one surviving owner retains the callback, last disposal stops notifications"
- id: SCN-checking-callback-disposal
given: "a live card's checking callback synchronously disposes the feed"
when: "badge notification returns to probe submission"
then: "no probe is submitted after unregister"
observable_outcomes:
- "callback event count/state sequence and external probe start count"
determinism_controls:
- "real badge/observer path, synchronous disposal callback and deferred probe IO"
anti_cheat_rationale:
prevents:
- "duplicated shared callbacks or dropping live callback owners"
- "post-disposal work submitted after synchronous reentrant teardown"
relaxation:
did_relax_any_assertion: false
if_true_explain_spec_basis: ""
```
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.
@@ -0,0 +1,7 @@
# Final callback ownership corrections
- Caller review shows app.boundStreamHealthBadgeHandler is shared across feeds; preserve global callback dedup with registration reference counts. Disposing one owner must retain the handler for another owner; final disposal removes it. Wrapping each registration separately would duplicate callbacks.
- Synchronous checking notifications can dispose/rerender the feed before queue submission. Measured1 start after disposal, expected0; check live membership after notification before creating queued work.
- Late observer scenario also asserts no disposed-card notifications reach a remaining registration, not just final badge state. This rejects a removed observer guard even when the reentrant guard still prevents queue submission.
- Final source has11 lifecycle scenarios;14 independent mutants were rerun against this exact product tree, all exit1 with1 failed/0 cancelled, then restored. Source outside gridHealth.js remains untouched.
- Corrected build and Chromium/Firefox built-dist/native-observer smoke both pass; no external requests or page errors. Actual media playback fallback and CPU/RSS remain unverified.
+20 -8
View File
@@ -21,14 +21,14 @@ import {
getLivenessProbePrefetchMargin,
} from "./constants.js";
const badgeUpdateListeners = new Set();
const badgeUpdateListeners = new Map();
function notifyBadgeUpdate(payload) {
if (!payload) {
return;
}
badgeUpdateListeners.forEach((listener) => {
badgeUpdateListeners.forEach((_owners, listener) => {
if (typeof listener !== "function") {
return;
}
@@ -479,7 +479,7 @@ function setBadge(card, state, details) {
notifyBadgeUpdate(payload);
}
function handleCardVisible({ card, pendingByCard, priority = 0 }) {
function handleCardVisible({ card, pendingByCard, liveCards, priority = 0 }) {
if (!(card instanceof HTMLElement)) {
return;
}
@@ -515,6 +515,9 @@ function handleCardVisible({ card, pendingByCard, priority = 0 }) {
}
setBadge(card, "checking");
// Badge callbacks can synchronously dispose/rerender the feed. Do not create
// a new request after its unregister hook has already run.
if (!liveCards.has(card)) return;
const encodedUrl = card.dataset.urlHealthUrl || "";
const webSeed = encodedUrl ? decodeURIComponent(encodedUrl) : "";
@@ -601,6 +604,7 @@ const gridCardObserver = createCardObserver({
handleCardVisible({
card,
pendingByCard: state.pendingByCard,
liveCards: state.liveCards,
priority: PRIORITY_BASELINE,
});
}
@@ -624,6 +628,7 @@ const gridCardObserver = createCardObserver({
handleCardVisible({
card,
pendingByCard: state.pendingByCard,
liveCards: state.liveCards,
priority,
});
},
@@ -634,16 +639,23 @@ export function attachHealthBadges(container, options = {}) {
return () => {};
}
const listener =
typeof options?.onUpdate === "function"
? (payload) => options.onUpdate(payload)
: null;
if (listener) badgeUpdateListeners.add(listener);
typeof options?.onUpdate === "function" ? options.onUpdate : null;
if (listener) {
badgeUpdateListeners.set(
listener,
(badgeUpdateListeners.get(listener) || 0) + 1
);
}
gridCardObserver.observe(container);
let disposed = false;
return () => {
if (disposed) return;
disposed = true;
if (listener) badgeUpdateListeners.delete(listener);
if (listener) {
const remaining = badgeUpdateListeners.get(listener) - 1;
if (remaining > 0) badgeUpdateListeners.set(listener, remaining);
else badgeUpdateListeners.delete(listener);
}
gridCardObserver.disconnect(container);
};
}
@@ -0,0 +1,10 @@
# client-reliability-3 final lifecycle review
- At712b05cf, final lint11/11 and build/artifact verification passed; full426-file run was interrupted130 after133 file exits, before file134 completed. No full green claimed for that partial run.
- Final caller review found app.boundStreamHealthBadgeHandler is shared across feed registrations. Preserve the existing global dedup contract with per-callback registration counts rather than per-registration callback wrappers. New shared-listener scenario checks one notification, survival after one owner disposes, and no notifications after the last owner disposes. Both duplicate-listener and unconditional-release mutants fail naturally (exit1,1 failed,0 cancelled).
- New actual callback-disposal reproduction: onUpdate disposes feed at checking, but old code still submits1 probe after unregister. Added post-notification live-card guard; missing-guard mutant fails naturally (exit1,1 failed,0 cancelled). No shared observer/VideoListView code edits needed; all callers read only.
- Corrected lifecycle suite11/11 passes naturally; total focused count now27 scenarios across4 files. New scoped correction does not change queue/cache/concurrency policy or active probes. Existing11 mutants plus these3 variants total14 rejected/restored variants.
- Browser/build/lint and full gates rerun below on final correction; earlier built-dist browser evidence belongs to the prior product tree and is not substituted for the corrected build.
- Final14-variant rerun against corrected listener/reentrant tree: all14 killed (each exit1/1 failed/0 cancelled), source restored. Late-entry case additionally asserts no stale disposed-card badge event reaches a remaining live listener.
- Corrected build/artifact verification exit0 and lint11/11 exit0. Corrected Chromium/Firefox native-observer smoke exit0: each4 active/0 obsolete starts, shared live healthy/disposed checking/0 stale updates, healthy hidden reveal, both source visibility fallbacks true,0 page errors/0 attempted external requests. Final log /tmp/client-reliability-3-browser-corrected.log; server/browsers closed and browser reservation released.
- Final direct focused runs after full mutant restoration: lifecycle11/11 (513.51ms), priority6/6 (381.59ms), visibility9/9 (394.34ms), observer1/1 (225.61ms), each exit0 naturally with0 failed/cancelled/skipped. Total27 scenarios across4 files. No source changes follow these focused checks.
+62 -1
View File
@@ -210,6 +210,59 @@ test("disposing one of two queued source owners preserves the live owner", async
);
});
test("disposal from a checking callback prevents probe submission", async (t) => {
const f = fixture(t);
const feed = f.feed([magnet()], {
onUpdate: ({ state }) => {
if (state === "checking") feed.dispose();
}
});
feed.observer.show();
await flush();
assert.equal(
f.calls.length,
0,
"checking notification must not submit work after synchronous disposal"
);
});
test("shared badge listener stays deduplicated until its last registration disposes", async (t) => {
const f = fixture(t);
const updates = [];
const listener = (event) => updates.push(event);
const first = f.feed([magnet()], { onUpdate: listener });
const live = f.feed([magnet()], { onUpdate: listener });
first.observer.show();
assert.equal(
updates.length,
1,
"same callback must receive each update once across registrations"
);
first.dispose();
updates.length = 0;
live.observer.show();
assert.equal(
updates.length,
1,
"disposing one registration must retain the shared callback"
);
f.calls[1].finish();
await flush();
assert.deepEqual(
updates.map((event) => event.state),
["checking", "healthy"]
);
live.dispose();
updates.length = 0;
const other = f.feed([magnet()]);
other.observer.show();
assert.equal(
updates.length,
0,
"last listener owner disposal must stop later 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();
@@ -317,9 +370,17 @@ test("active probe remains shared while disposed owners and late observer entrie
const first = f.feed([source]);
first.observer.show();
first.dispose();
const liveUpdates = [];
const live = f.feed([source], {
onUpdate: (event) => liveUpdates.push(event)
});
first.observer.show(); // An already-delivered entry may arrive after disconnect.
const live = f.feed([source]);
live.observer.show();
assert.equal(
liveUpdates.some((event) => event.card === first.cards[0]),
false,
"late observer entries must not emit disposed-card badge events"
);
assert.equal(
f.calls.length,
1,
@@ -0,0 +1,6 @@
- [x] Actual disposal reproduction1→0 obsolete starts, queued/shared/active lifecycle preservation.
- [x] Caller review preserves shared-handler dedup and checks synchronous checking-callback disposal1→0 starts.
- [x] Focused27/27 across4 files (11 lifecycle+6 priority+9 visibility+1 observer),14 reverted mutants killed.
- [x] Corrected product lint11/11, build/artifact verification and Chromium/Firefox built-dist/native-observer smoke pass; browser released.
- [ ] Commit corrected tree, merge unstable, restart full426-file60s gate and final lint; normal queue/durable landing/ancestry, dispatch landing event, close/final report.
- Not qualified: actual media/torrent fallback, live peers, CPU/RSS, URL cancellation or active abort. Dispatch owns preview refresh. No new item without routing.