mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
client-reliability-3: preserve shared badge callbacks and guard reentrant disposal
This commit is contained in:
1 parent
712b05cf87
commit
607d5ce1a1
6 files changed
+134
-9
No files matched your search
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user