From f2e4c5b684f1e59d5138ac4f4a61d35dc5ea3727 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:16:30 +0200 Subject: [PATCH] fix(orbit): tear down session on server switch + raise cleanup TTL Co-Authored-By: Claude Opus 4.7 (1M context) --- src/utils/orbit.ts | 12 ++++++------ src/utils/switchActiveServer.ts | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/utils/orbit.ts b/src/utils/orbit.ts index 5f0cd777..8f9cc8c8 100644 --- a/src/utils/orbit.ts +++ b/src/utils/orbit.ts @@ -732,13 +732,13 @@ export async function sweepGuestOutboxes(sid: string, hostUsername: string): Pro export const ORBIT_HEARTBEAT_ALIVE_MS = 30_000; /** - * Grace window for the app-start orphan sweep. Has to be comfortably - * larger than the outbox heartbeat interval (10 s) and the session state - * tick (2.5 s) so a session running on the user's other device doesn't - * get deleted on a transient slow tick. 2× ALIVE window is the minimum - * sane value. + * 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 = 60_000; +export const ORBIT_ORPHAN_TTL_MS = 5 * 60_000; /** * Legacy / fallback shuffle cadence. New sessions store their own interval diff --git a/src/utils/switchActiveServer.ts b/src/utils/switchActiveServer.ts index fe13552c..555cd4c6 100644 --- a/src/utils/switchActiveServer.ts +++ b/src/utils/switchActiveServer.ts @@ -1,11 +1,31 @@ import { pingWithCredentials, scheduleInstantMixProbeForServer } from '../api/subsonic'; import type { ServerProfile } from '../store/authStore'; import { useAuthStore } from '../store/authStore'; +import { useOrbitStore } from '../store/orbitStore'; +import { endOrbitSession, leaveOrbitSession } from './orbit'; export async function switchActiveServer(server: ServerProfile): Promise { try { const ping = await pingWithCredentials(server.url, server.username, server.password); if (!ping.ok) return false; + + // Tear down any active Orbit session before we actually switch. The + // session's playlists live on the *old* server — once we flip the + // active server, every API call from the orbit hooks would hit the + // wrong backend, heartbeats would silently fail, and the next + // app-start cleanup would prune the still-live session as stale. + // Capped at 1.5 s so a slow network doesn't freeze the UI. + const role = useOrbitStore.getState().role; + if (role === 'host' || role === 'guest') { + const teardown = role === 'host' ? endOrbitSession() : leaveOrbitSession(); + await Promise.race([ + teardown.catch(() => {}), + new Promise(r => setTimeout(r, 1500)), + ]); + // Ensure local store is idle even if the remote call timed out. + useOrbitStore.getState().reset(); + } + const identity = { type: ping.type, serverVersion: ping.serverVersion,