chore(orbit): guard bulk queue operations

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-24 11:24:46 +02:00
parent f8bcd57ed4
commit 7ba7d6bf25
9 changed files with 191 additions and 7 deletions
+47
View File
@@ -0,0 +1,47 @@
import { create } from 'zustand';
let _resolve: ((accepted: boolean) => void) | null = null;
interface ConfirmRequest {
title: string;
message: string;
confirmLabel: string;
cancelLabel?: string;
danger?: boolean;
}
interface ConfirmModalStore extends ConfirmRequest {
isOpen: boolean;
request: (req: ConfirmRequest) => Promise<boolean>;
confirm: () => void;
cancel: () => void;
}
export const useConfirmModalStore = create<ConfirmModalStore>(set => ({
isOpen: false,
title: '',
message: '',
confirmLabel: '',
cancelLabel: undefined,
danger: false,
request: (req) =>
new Promise<boolean>(resolve => {
// If a previous prompt is still pending, treat the old one as cancelled.
if (_resolve) _resolve(false);
_resolve = resolve;
set({ isOpen: true, ...req });
}),
confirm: () => {
_resolve?.(true);
_resolve = null;
set({ isOpen: false });
},
cancel: () => {
_resolve?.(false);
_resolve = null;
set({ isOpen: false });
},
}));
+31 -6
View File
@@ -10,6 +10,7 @@ import { lastfmScrobble, lastfmUpdateNowPlaying, lastfmLoveTrack, lastfmUnloveTr
import { useAuthStore } from './authStore';
import { useOfflineStore } from './offlineStore';
import { useHotCacheStore } from './hotCacheStore';
import { orbitBulkGuard } from '../utils/orbitBulkGuard';
export interface Track {
id: string;
@@ -168,7 +169,9 @@ interface PlayerState {
setUserRatingOverride: (id: string, rating: number) => void;
playRadio: (station: InternetRadioStation) => void;
playTrack: (track: Track, queue?: Track[], manual?: boolean) => void;
/** `_orbitConfirmed` is an internal bypass flag — callers outside the
* orbit bulk-gate should leave it `undefined`. */
playTrack: (track: Track, queue?: Track[], manual?: boolean, _orbitConfirmed?: boolean) => void;
/** Queue becomes `[track]` only; if already on this track, does not restart `audio_play`. */
reseedQueueForInstantMix: (track: Track) => void;
pause: () => void;
@@ -193,8 +196,8 @@ interface PlayerState {
setVolume: (v: number) => void;
updateReplayGainForCurrentTrack: () => void;
setProgress: (t: number, duration: number) => void;
enqueue: (tracks: Track[]) => void;
enqueueAt: (tracks: Track[], insertIndex: number) => void;
enqueue: (tracks: Track[], _orbitConfirmed?: boolean) => void;
enqueueAt: (tracks: Track[], insertIndex: number, _orbitConfirmed?: boolean) => void;
enqueueRadio: (tracks: Track[], artistId?: string) => void;
setRadioArtistId: (artistId: string) => void;
/** For Lucky Mix: drop upcoming tail; keep the currently playing item only. */
@@ -1206,7 +1209,17 @@ export const usePlayerStore = create<PlayerState>()(
},
// ── playTrack ────────────────────────────────────────────────────────────
playTrack: (track, queue, manual = true) => {
playTrack: (track, queue, manual = true, _orbitConfirmed = false) => {
// Orbit bulk-gate: a >1-track queue inside an active session is
// a "Play All / Play Album"-style action — confirm with the user
// before it lands on every guest's player.
if (!_orbitConfirmed && queue && queue.length > 1) {
void orbitBulkGuard(queue.length).then(ok => {
if (ok) get().playTrack(track, queue, manual, true);
});
return;
}
// Ghost-command guard: if a gapless switch happened within 500 ms,
// this playTrack call is likely a stale IPC echo — suppress it.
if (Date.now() - lastGaplessSwitchTime < 500) {
@@ -1753,7 +1766,13 @@ export const usePlayerStore = create<PlayerState>()(
},
// ── queue management ─────────────────────────────────────────────────────
enqueue: (tracks) => {
enqueue: (tracks, _orbitConfirmed = false) => {
if (!_orbitConfirmed && tracks.length > 1) {
void orbitBulkGuard(tracks.length).then(ok => {
if (ok) get().enqueue(tracks, true);
});
return;
}
set(state => {
// Insert before the first upcoming auto-added track so the
// "Added automatically" separator always stays at the boundary.
@@ -1796,7 +1815,13 @@ export const usePlayerStore = create<PlayerState>()(
});
},
enqueueAt: (tracks, insertIndex) => {
enqueueAt: (tracks, insertIndex, _orbitConfirmed = false) => {
if (!_orbitConfirmed && tracks.length > 1) {
void orbitBulkGuard(tracks.length).then(ok => {
if (ok) get().enqueueAt(tracks, insertIndex, true);
});
return;
}
set(state => {
const idx = Math.max(0, Math.min(insertIndex, state.queue.length));
const newQueue = [