mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
fix(orbit): cleanup regex, state-blob budget, radio top-up lockout (#1155)
* fix(orbit): stop cleanup sweep from deleting live sessions on other devices The orphan-cleanup regex anchored the trailing `__` inside the optional `_from_…` group, so the canonical session playlist name (`__psyorbit_<sid>__`) never matched. It then fell into the "unrecognised → prune" branch, which deletes unconditionally and bypasses the orphan-TTL grace that exists precisely to protect a session running on the user's other device. Opening Psysonic on a second device wiped the first device's live session out from under its guests. Move the trailing `__` outside the optional group so both the session and outbox names match; the existing sid/TTL/ended logic then applies as intended. Add a behavioural test covering keep-fresh / prune-stale / prune-ended / skip-current / outbox / corrupt / foreign-owner cases. * fix(orbit): keep host publishing when the state blob grows past budget OrbitState.queue (the suggestion/attribution history) was append-only at the sweep-fold step and never bounded. On a long session it grew until the serialised blob exceeded ORBIT_STATE_MAX_BYTES; serialiseOrbitState then threw, writeOrbitState's caller swallowed the error and retried the same over-budget state every tick, so the host silently stopped publishing and guests froze into a host-timeout. Two layers: - Cap queue at ORBIT_QUEUE_HISTORY_LIMIT in applyOutboxSnapshotsToState, evicting oldest-by-addedAt (robust to the periodic queue shuffle). The dropped tracks have long since played, so their attribution/dedupe entry is dead weight. - Add serialiseOrbitStateForWire: a budget-aware serialiser that sheds oldest history, then the play-queue tail, on a copy before giving up. writeOrbitState now uses it, so a transient over-budget tick degrades the published blob instead of taking the host offline. Local store state is untouched. Tested: history cap eviction order, wire-trim byte budget + oldest-first drop, play-queue fallback, and within-budget passthrough. * fix(orbit): lock out the proactive radio top-up during a session The ≤2-remaining radio top-up in runNext lacked the isInOrbitSession() guard that its infinite-queue sibling has at both entry and resolution. A guest who joined with residual radioAdded tracks (before the first syncToHost replaces the queue) could fire it, appending up to 10 unrelated radio tracks and trimming queue history — drifting the guest off the host's playlist. Add the guard at entry and re-check inside the resolution .then(), mirroring the infinite-queue branch; the existing finally() still clears the fetching flag. The queue-exhausted radio fallback already returns early in Orbit, so only this mid-queue path was unguarded. * docs(changelog): add Orbit cleanup / state-budget / radio fixes (#1155) * docs(changelog): move Orbit fixes to bottom of Fixed section (#1155)
This commit is contained in:
+10
-3
@@ -111,9 +111,12 @@ export function runNext(set: SetState, get: GetState, manual: boolean): void {
|
||||
}).catch(() => {}).finally(() => { setInfiniteQueueFetching(false); });
|
||||
}
|
||||
}
|
||||
// Proactively top up radio tracks when ≤ 2 remain — always, regardless
|
||||
// of infinite queue setting.
|
||||
if (nextRef.radioAdded && !isRadioFetching()) {
|
||||
// Proactively top up radio tracks when ≤ 2 remain — independent of the
|
||||
// infinite-queue setting, but still skipped in Orbit: the radio top-up
|
||||
// appends unrelated tracks and trims queue history, which would drift a
|
||||
// guest off the host's playlist (same rationale as the infinite-queue
|
||||
// branch above).
|
||||
if (nextRef.radioAdded && !isRadioFetching() && !isInOrbitSession()) {
|
||||
const remainingRadio = queueItems.slice(nextIdx + 1).filter(r => r.radioAdded).length;
|
||||
if (remainingRadio <= 2) {
|
||||
// H2: nextTrack may be a placeholder if its ref is still cold — empty
|
||||
@@ -133,6 +136,10 @@ export function runNext(set: SetState, get: GetState, manual: boolean): void {
|
||||
setRadioFetching(true);
|
||||
Promise.all([getSimilarSongs2(seedArtistId), getTopSongs(seedArtistName)])
|
||||
.then(([similar, top]) => {
|
||||
// Re-check — the user may have joined an Orbit session between
|
||||
// scheduling this fetch and its resolution (mirrors the
|
||||
// infinite-queue branch). The finally() still clears the flag.
|
||||
if (isInOrbitSession()) return;
|
||||
const existingIds = new Set(get().queueItems.map(r => r.trackId));
|
||||
// Lead with similar (other artists) for variety; top tracks
|
||||
// of the upcoming artist are only a fallback when similar
|
||||
|
||||
Reference in New Issue
Block a user