chore(orbit): auto-leave on guest side after prolonged host silence

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-24 17:43:34 +02:00
parent 17bcac7155
commit 01e148d082
4 changed files with 32 additions and 6 deletions
+11 -6
View File
@@ -22,21 +22,26 @@ export default function OrbitExitModal() {
const sessionName = useOrbitStore(s => s.state?.name);
const hostName = useOrbitStore(s => s.state?.host);
const isEnded = phase === 'ended';
const isKicked = phase === 'error' && errorMessage === 'kicked';
const isRemoved = phase === 'error' && errorMessage === 'removed';
if (!isEnded && !isKicked && !isRemoved) return null;
const isEnded = phase === 'ended';
const isKicked = phase === 'error' && errorMessage === 'kicked';
const isRemoved = phase === 'error' && errorMessage === 'removed';
const isHostTimeout = phase === 'error' && errorMessage === 'host-timeout';
if (!isEnded && !isKicked && !isRemoved && !isHostTimeout) return null;
const title = isKicked
? t('orbit.exitKickedTitle')
: isRemoved
? t('orbit.exitRemovedTitle')
: t('orbit.exitEndedTitle');
: isHostTimeout
? t('orbit.exitHostTimeoutTitle')
: t('orbit.exitEndedTitle');
const body = isKicked
? t('orbit.exitKickedBody', { host: hostName ?? '', name: sessionName ?? '' })
: isRemoved
? t('orbit.exitRemovedBody', { host: hostName ?? '', name: sessionName ?? '' })
: t('orbit.exitEndedBody', { name: sessionName ?? '' });
: isHostTimeout
? t('orbit.exitHostTimeoutBody', { host: hostName ?? '', name: sessionName ?? '' })
: t('orbit.exitEndedBody', { name: sessionName ?? '' });
const onOk = async () => {
try {
+17
View File
@@ -28,6 +28,13 @@ import { orbitOutboxPlaylistName, estimateLivePosition, type OrbitState } from '
const STATE_READ_TICK_MS = 2_500;
const HEARTBEAT_TICK_MS = 10_000;
/**
* Host must be quiet (no state writes) for this long before we treat the
* session as dead and auto-leave. Well above any normal network blip —
* reconnects inside this window are silent. Tuned per user decision:
* manual exits have priority, short reconnects never trigger auto-close.
*/
const HOST_TIMEOUT_MS = 5 * 60_000;
export function useOrbitGuest(): void {
const role = useOrbitStore(s => s.role);
@@ -129,6 +136,16 @@ export function useOrbitGuest(): void {
useOrbitStore.getState().setState(state);
// Auto-leave after prolonged host silence. We keep polling as long as
// state reads succeed (short reconnects are silent), but if the host
// hasn't written a fresh state blob for > HOST_TIMEOUT_MS we treat the
// session as effectively dead and surface the exit modal. Manual exit
// still works instantly — the bar's X button short-circuits this path.
if (state.positionAt > 0 && (Date.now() - state.positionAt) > HOST_TIMEOUT_MS) {
useOrbitStore.getState().setError('host-timeout');
return;
}
// Reconcile pending guest suggestions against the host's *playable*
// queue — NOT `state.queue`, which is the suggestion history (every
// submission lands there immediately, even under manual-approval mode
+2
View File
@@ -1566,6 +1566,8 @@ export const deTranslation = {
exitKickedTitle: 'Du wurdest aus der Session gebannt',
exitRemovedTitle: 'Du wurdest aus der Session entfernt',
exitEndedTitle: 'Der Gastgeber hat die Session beendet',
exitHostTimeoutTitle: 'Host nicht mehr erreichbar',
exitHostTimeoutBody: '{{host}} hat sich länger nicht mehr gemeldet — „{{name}}" wurde auf deiner Seite beendet. Über den Einladungslink kannst du jederzeit wieder beitreten.',
exitKickedBody: '{{host}} hat dich aus „{{name}}" gebannt. Beitritt nicht mehr möglich.',
exitRemovedBody: '{{host}} hat dich aus „{{name}}" entfernt. Du kannst jederzeit über den Einladungslink wieder beitreten.',
exitEndedBody: '„{{name}}" ist zu Ende. Hoffentlich war\'s schön.',
+2
View File
@@ -1569,6 +1569,8 @@ export const enTranslation = {
exitKickedTitle: 'You were banned from the session',
exitRemovedTitle: 'You were removed from the session',
exitEndedTitle: 'The host ended the session',
exitHostTimeoutTitle: 'Host went silent',
exitHostTimeoutBody: "{{host}} hasn't sent an update for a while, so \"{{name}}\" was closed on your end. You can re-join any time with the invite link.",
exitKickedBody: '{{host}} banned you from "{{name}}". You can\'t re-join.',
exitRemovedBody: '{{host}} removed you from "{{name}}". You can re-join any time via the invite link.',
exitEndedBody: '"{{name}}" has ended. Hope you had fun.',