mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
fix(orbit): pending counter ignored merged/declined items
The "X / Y pending" counter in the queue head and the guest-side gate-check both used `state.queue.filter(non-host).length`, which is the *history* count — items the host has already approved or declined still sit in `state.queue` for attribution lookup, so the counter never decreased. Reported as "3 / 4 pending" with no actual rows in the approval list. The merged / declined sets only exist in the host's local store, so the guest can't filter them out itself. Solution: the host writes an authoritative `pendingApprovalCount` into the state blob each tick; guests (and the host's own UI) read it directly, with a fallback to the old over-counting behaviour for any older client that doesn't write the field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,14 @@ export interface OrbitState {
|
|||||||
* before they reach the approval list. Symmetric — host can re-enable.
|
* before they reach the approval list. Symmetric — host can re-enable.
|
||||||
*/
|
*/
|
||||||
suggestionBlocked?: string[];
|
suggestionBlocked?: string[];
|
||||||
|
/**
|
||||||
|
* Authoritative count of suggestions actually waiting on host action right
|
||||||
|
* now (`state.queue` minus host-authored, merged, declined). The host
|
||||||
|
* rewrites it every tick — guests can't compute it themselves because the
|
||||||
|
* merged/declined sets live in the host's local store. Older clients that
|
||||||
|
* don't write the field fall back to a `state.queue` count in the UI.
|
||||||
|
*/
|
||||||
|
pendingApprovalCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ export default function OrbitQueueHead({ state }: Props) {
|
|||||||
const showPresence = role === 'guest' && state.positionAt > 0;
|
const showPresence = role === 'guest' && state.positionAt > 0;
|
||||||
const hostAway = showPresence && (nowMs - state.positionAt) > HOST_AWAY_THRESHOLD_MS;
|
const hostAway = showPresence && (nowMs - state.positionAt) > HOST_AWAY_THRESHOLD_MS;
|
||||||
const cap = state.settings?.maxPending ?? 0;
|
const cap = state.settings?.maxPending ?? 0;
|
||||||
// Approximate visible-pending count — same heuristic as evaluateOrbitSuggestGate.
|
// Authoritative count comes from the host's own tick (state.pendingApprovalCount).
|
||||||
// Conservative for guests (over-counts declined entries) but the host's
|
// Older clients that predate that field fall back to the raw queue length
|
||||||
// own queue view sees the exact number because it has the merged/declined
|
// — that one over-counts because merged/declined items stay in state.queue
|
||||||
// sets locally; we don't bother surfacing the exact count here either way
|
// as history, but it's the best a non-host client can do.
|
||||||
// since guests just need to know if they're near the cap.
|
|
||||||
const pendingCount = cap > 0
|
const pendingCount = cap > 0
|
||||||
? state.queue.filter(q => q.addedBy !== state.host).length
|
? (state.pendingApprovalCount
|
||||||
|
?? state.queue.filter(q => q.addedBy !== state.host).length)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -130,11 +130,24 @@ export function useOrbitHost(): void {
|
|||||||
trackId: t.id,
|
trackId: t.id,
|
||||||
addedBy: suggesterByTrack.get(t.id) ?? base.host,
|
addedBy: suggesterByTrack.get(t.id) ?? base.host,
|
||||||
}));
|
}));
|
||||||
|
// Authoritative pending count — same predicate the approval list uses
|
||||||
|
// (state.queue minus host-authored, minus merged, minus declined). We
|
||||||
|
// recompute fresh each tick from the *current* store sets so an approve
|
||||||
|
// / decline that happened mid-sweep is reflected on the very next push.
|
||||||
|
const mergedNow = new Set(useOrbitStore.getState().mergedSuggestionKeys);
|
||||||
|
const declinedNow = new Set(useOrbitStore.getState().declinedSuggestionKeys);
|
||||||
|
const pendingApprovalCount = afterShuffle.queue.filter(q =>
|
||||||
|
q.addedBy !== afterShuffle.host
|
||||||
|
&& !mergedNow.has(suggestionKey(q))
|
||||||
|
&& !declinedNow.has(suggestionKey(q))
|
||||||
|
).length;
|
||||||
|
|
||||||
const next: OrbitState = {
|
const next: OrbitState = {
|
||||||
...afterShuffle,
|
...afterShuffle,
|
||||||
...snapshotPlayerPatch(base.host),
|
...snapshotPlayerPatch(base.host),
|
||||||
playQueue,
|
playQueue,
|
||||||
playQueueTotal: upcoming.length,
|
playQueueTotal: upcoming.length,
|
||||||
|
pendingApprovalCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5) Commit locally + push remote.
|
// 5) Commit locally + push remote.
|
||||||
|
|||||||
+8
-3
@@ -480,9 +480,14 @@ export function evaluateOrbitSuggestGate(): { allowed: boolean; reason: OrbitSug
|
|||||||
}
|
}
|
||||||
const cap = state.settings?.maxPending ?? 0;
|
const cap = state.settings?.maxPending ?? 0;
|
||||||
if (cap > 0) {
|
if (cap > 0) {
|
||||||
const inState = state.queue.filter(q => q.addedBy !== state.host).length;
|
// Prefer the host-pushed authoritative count when present; fall back to a
|
||||||
const total = inState + pendingSuggestions.length;
|
// raw queue scan for older hosts. Add the local guest's pending list on
|
||||||
if (total >= cap) return { allowed: false, reason: 'cap-reached' };
|
// top — those are tracks the host hasn't swept in yet.
|
||||||
|
const knownPending = state.pendingApprovalCount
|
||||||
|
?? state.queue.filter(q => q.addedBy !== state.host).length;
|
||||||
|
if (knownPending + pendingSuggestions.length >= cap) {
|
||||||
|
return { allowed: false, reason: 'cap-reached' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { allowed: true, reason: null };
|
return { allowed: true, reason: null };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user