diff --git a/crates/qbz-core/src/core.rs b/crates/qbz-core/src/core.rs index 09a1f29e2..be4816d6d 100644 --- a/crates/qbz-core/src/core.rs +++ b/crates/qbz-core/src/core.rs @@ -156,6 +156,10 @@ impl QbzCore { let queue = self.queue.write().await; queue.set_shuffle(enabled); self.emit(CoreEvent::ShuffleChanged { enabled }).await; + self.emit(CoreEvent::QueueUpdated { + state: queue.get_state(), + }) + .await; } /// Set shuffle mode using an authoritative order. @@ -163,6 +167,10 @@ impl QbzCore { let queue = self.queue.write().await; queue.set_shuffle_with_order(enabled, shuffle_order); self.emit(CoreEvent::ShuffleChanged { enabled }).await; + self.emit(CoreEvent::QueueUpdated { + state: queue.get_state(), + }) + .await; } /// Toggle shuffle and return new state @@ -175,6 +183,10 @@ impl QbzCore { enabled: new_enabled, }) .await; + self.emit(CoreEvent::QueueUpdated { + state: queue.get_state(), + }) + .await; new_enabled } diff --git a/src/lib/stores/queueStore.test.ts b/src/lib/stores/queueStore.test.ts index 9cf8b67ea..332d5e7c8 100644 --- a/src/lib/stores/queueStore.test.ts +++ b/src/lib/stores/queueStore.test.ts @@ -179,57 +179,34 @@ describe('queueStore', () => { }); describe('toggleShuffle', () => { - it('should toggle shuffle on', async () => { - mockedInvoke - .mockResolvedValueOnce(undefined) - .mockResolvedValueOnce({ - current_track: null, - current_index: null, - upcoming: [], - history: [], - shuffle: true, - repeat: 'Off', - total_tracks: 0 - }); + it('should request shuffle on without mutating local state optimistically', async () => { + mockedInvoke.mockResolvedValueOnce(undefined); const result = await toggleShuffle(); expect(result).toEqual({ success: true, enabled: true }); - expect(getIsShuffle()).toBe(true); + expect(getIsShuffle()).toBe(false); expect(mockedInvoke).toHaveBeenCalledWith('v2_toggle_shuffle'); + expect(mockedInvoke).toHaveBeenCalledTimes(1); }); - it('should toggle shuffle off', async () => { - // First toggle on - mockedInvoke - .mockResolvedValueOnce(undefined) - .mockResolvedValueOnce({ - current_track: null, - current_index: null, - upcoming: [], - history: [], - shuffle: true, - repeat: 'Off', - total_tracks: 0 - }); - await toggleShuffle(); + it('should request shuffle off from the current authoritative state', async () => { + mockedInvoke.mockResolvedValueOnce({ + current_track: null, + current_index: null, + upcoming: [], + history: [], + shuffle: true, + repeat: 'Off', + total_tracks: 0 + }); + await syncQueueState(); - // Then toggle off - mockedInvoke - .mockResolvedValueOnce(undefined) - .mockResolvedValueOnce({ - current_track: null, - current_index: null, - upcoming: [], - history: [], - shuffle: false, - repeat: 'Off', - total_tracks: 0 - }); + mockedInvoke.mockResolvedValueOnce(undefined); const result = await toggleShuffle(); expect(result).toEqual({ success: true, enabled: false }); - expect(getIsShuffle()).toBe(false); + expect(getIsShuffle()).toBe(true); }); it('should revert on error', async () => { @@ -363,7 +340,7 @@ describe('queueStore', () => { expect(getRepeatMode()).toBe('all'); }); - it('should sync queue state after queue:shuffle-changed', async () => { + it('should ignore queue:shuffle-changed until queue:updated arrives', async () => { const eventHandlers = new Map void>(); mockedListen.mockImplementation(async (eventName, handler) => { eventHandlers.set(String(eventName), handler as (event: { payload: unknown }) => void); @@ -372,25 +349,6 @@ describe('queueStore', () => { }; }); - mockedInvoke.mockResolvedValueOnce({ - current_track: null, - current_index: 0, - upcoming: [ - { - id: 7, - title: 'Shuffled Track', - artist: 'Artist', - album: 'Album', - duration_secs: 220, - artwork_url: null - } - ], - history: [], - shuffle: true, - repeat: 'Off', - total_tracks: 10 - }); - await startQueueEventListener(); const shuffleChanged = eventHandlers.get('queue:shuffle-changed'); @@ -398,11 +356,10 @@ describe('queueStore', () => { shuffleChanged?.({ payload: true }); await Promise.resolve(); - await Promise.resolve(); - expect(mockedInvoke).toHaveBeenCalledWith('v2_get_queue_state'); - expect(getIsShuffle()).toBe(true); - expect(getQueue().map(track => track.id)).toEqual(['7']); + expect(mockedInvoke).not.toHaveBeenCalledWith('v2_get_queue_state'); + expect(getIsShuffle()).toBe(false); + expect(getQueue()).toEqual([]); }); }); }); diff --git a/src/lib/stores/queueStore.ts b/src/lib/stores/queueStore.ts index bc75f0c55..9acb53640 100644 --- a/src/lib/stores/queueStore.ts +++ b/src/lib/stores/queueStore.ts @@ -239,18 +239,12 @@ export async function syncQueueState(): Promise { */ export async function toggleShuffle(): Promise<{ success: boolean; enabled: boolean }> { const newState = !isShuffle; - isShuffle = newState; - notifyListeners(); try { await invoke('v2_toggle_shuffle'); - await syncQueueState(); return { success: true, enabled: newState }; } catch (err) { console.error('Failed to set shuffle:', err); - // Revert on error - isShuffle = !newState; - notifyListeners(); return { success: false, enabled: !newState }; } } @@ -490,11 +484,9 @@ export async function startQueueEventListener(): Promise { const shuffleChangedUnlisten = await listen('queue:shuffle-changed', (event) => { console.log('[Queue] Received queue:shuffle-changed event:', event.payload); - isShuffle = event.payload; - notifyListeners(); - syncQueueState().catch(err => - console.error('[Queue] Failed to sync queue after queue:shuffle-changed event:', err) - ); + // Shuffle changes must be reflected by the authoritative queue:updated + // payload, not by forcing a local resync that can capture an + // intermediate non-authoritative order. }); const repeatChangedUnlisten = await listen('queue:repeat-changed', (event) => {