mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
2c9b2eeb46
* 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)
47 lines
1.9 KiB
TypeScript
47 lines
1.9 KiB
TypeScript
/**
|
|
* Orbit cadence + TTL constants.
|
|
*
|
|
* Centralised so host/guest/state-math code reads from the same source of
|
|
* truth. Defaults are deliberately chosen against observed Navidrome poll
|
|
* cadences and real-world flake budgets.
|
|
*/
|
|
|
|
/** How long we consider a heartbeat still fresh. Longer than the guest tick so a single missed beat is tolerated. */
|
|
export const ORBIT_HEARTBEAT_ALIVE_MS = 30_000;
|
|
|
|
/**
|
|
* Grace window for the app-start orphan sweep. A session on the user's
|
|
* other device or a browser that briefly restarted must NOT be deleted
|
|
* by this sweep. 5 min matches the guest-side host-timeout threshold:
|
|
* if a session is silent for that long, it's fair to treat it as dead;
|
|
* anything shorter is a real restart and must survive.
|
|
*/
|
|
export const ORBIT_ORPHAN_TTL_MS = 5 * 60_000;
|
|
|
|
/**
|
|
* Legacy / fallback shuffle cadence. New sessions store their own interval
|
|
* in `OrbitState.settings.shuffleIntervalMin`; `effectiveShuffleIntervalMs`
|
|
* resolves that against this constant for sessions created before the
|
|
* field existed.
|
|
*/
|
|
export const ORBIT_SHUFFLE_INTERVAL_MS = 15 * 60_000;
|
|
|
|
/**
|
|
* Upper bound on `OrbitState.queue` (the suggestion/attribution history).
|
|
* The list is append-only at the sweep-fold step, so without a cap a long
|
|
* session grows it without limit until the serialised blob blows past
|
|
* `ORBIT_STATE_MAX_BYTES` and the host silently stops publishing. We keep the
|
|
* most-recently-added entries (oldest dropped first) — those cover the
|
|
* upcoming play queue, which is all the attribution lookup needs. The wire
|
|
* serialiser trims further per-write if the blob still overflows.
|
|
*/
|
|
export const ORBIT_QUEUE_HISTORY_LIMIT = 64;
|
|
|
|
/**
|
|
* How long a soft-`removed` marker stays in the state blob. Long enough for
|
|
* the affected guest's 2.5 s read tick to surface the modal even after a
|
|
* one-tick miss; short enough that the marker doesn't bloat state if the
|
|
* guest never reconnects.
|
|
*/
|
|
export const ORBIT_REMOVED_TTL_MS = 60_000;
|