mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
refactor(player): E.1 — extract queue-identity helpers to utils/ (#563)
Four pure helpers move out of playerStore: normalizeAnalysisTrackId, sameQueueTrackId, queuesStructuralEqual, shallowCloneQueueTracks. Adds focused unit tests for the stream:-prefix normalization and the no-op detection that prevents unnecessary queue rewrites. Behaviour preserved verbatim. playerStore 3618 → 3598 LOC.
This commit is contained in:
committed by
GitHub
parent
f0971d5108
commit
dcf3dd98e0
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Pure helpers extracted from playerStore. The interesting behaviour is the
|
||||
* `stream:` prefix normalization (Rust events sometimes wrap track ids when
|
||||
* routing through the HTTP source) and the no-op detection in
|
||||
* `queuesStructuralEqual` that prevents unnecessary store rewrites.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { Track } from '../store/playerStore';
|
||||
import {
|
||||
normalizeAnalysisTrackId,
|
||||
queuesStructuralEqual,
|
||||
sameQueueTrackId,
|
||||
shallowCloneQueueTracks,
|
||||
} from './queueIdentity';
|
||||
|
||||
function track(id: string, overrides: Partial<Track> = {}): Track {
|
||||
return {
|
||||
id,
|
||||
title: `Title ${id}`,
|
||||
artist: 'Artist',
|
||||
album: 'Album',
|
||||
albumId: 'A',
|
||||
duration: 180,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('normalizeAnalysisTrackId', () => {
|
||||
it('strips the stream: prefix', () => {
|
||||
expect(normalizeAnalysisTrackId('stream:abc123')).toBe('abc123');
|
||||
});
|
||||
|
||||
it('returns bare ids unchanged', () => {
|
||||
expect(normalizeAnalysisTrackId('abc123')).toBe('abc123');
|
||||
});
|
||||
|
||||
it('returns null for null / undefined / empty', () => {
|
||||
expect(normalizeAnalysisTrackId(null)).toBeNull();
|
||||
expect(normalizeAnalysisTrackId(undefined)).toBeNull();
|
||||
expect(normalizeAnalysisTrackId('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sameQueueTrackId', () => {
|
||||
it('matches bare ids', () => {
|
||||
expect(sameQueueTrackId('a', 'a')).toBe(true);
|
||||
expect(sameQueueTrackId('a', 'b')).toBe(false);
|
||||
});
|
||||
|
||||
it('matches across stream: prefix mismatch', () => {
|
||||
expect(sameQueueTrackId('stream:a', 'a')).toBe(true);
|
||||
expect(sameQueueTrackId('a', 'stream:a')).toBe(true);
|
||||
expect(sameQueueTrackId('stream:a', 'stream:a')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when either side is null', () => {
|
||||
expect(sameQueueTrackId(null, 'a')).toBe(false);
|
||||
expect(sameQueueTrackId('a', null)).toBe(false);
|
||||
expect(sameQueueTrackId(null, null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('queuesStructuralEqual', () => {
|
||||
it('returns true for same ids in same order', () => {
|
||||
expect(queuesStructuralEqual([track('a'), track('b')], [track('a'), track('b')])).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when one side wraps ids with stream:', () => {
|
||||
expect(queuesStructuralEqual(
|
||||
[track('a'), track('b')],
|
||||
[track('stream:a'), track('stream:b')],
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for different lengths', () => {
|
||||
expect(queuesStructuralEqual([track('a')], [track('a'), track('b')])).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for any id mismatch', () => {
|
||||
expect(queuesStructuralEqual([track('a'), track('b')], [track('a'), track('c')])).toBe(false);
|
||||
});
|
||||
|
||||
it('treats empty queues as equal', () => {
|
||||
expect(queuesStructuralEqual([], [])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shallowCloneQueueTracks', () => {
|
||||
it('returns a new array with new objects (callers can mutate freely)', () => {
|
||||
const original = [track('a'), track('b')];
|
||||
const cloned = shallowCloneQueueTracks(original);
|
||||
expect(cloned).not.toBe(original);
|
||||
expect(cloned[0]).not.toBe(original[0]);
|
||||
expect(cloned[1]).not.toBe(original[1]);
|
||||
expect(cloned).toEqual(original);
|
||||
});
|
||||
|
||||
it('preserves all fields', () => {
|
||||
const original = [track('a', { coverArt: 'cover', userRating: 5, autoAdded: true })];
|
||||
const cloned = shallowCloneQueueTracks(original);
|
||||
expect(cloned[0]).toEqual(original[0]);
|
||||
});
|
||||
|
||||
it('handles empty queues', () => {
|
||||
expect(shallowCloneQueueTracks([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Track } from '../store/playerStore';
|
||||
|
||||
/**
|
||||
* Strip the `stream:` prefix that some Rust events attach to track ids when
|
||||
* they're routed through the HTTP source. Both forms identify the same track,
|
||||
* so equality and structural-diff checks need to normalize first.
|
||||
*/
|
||||
export function normalizeAnalysisTrackId(trackId?: string | null): string | null {
|
||||
if (!trackId) return null;
|
||||
if (trackId.startsWith('stream:')) return trackId.slice('stream:'.length);
|
||||
return trackId;
|
||||
}
|
||||
|
||||
/** Compare track ids across `stream:` / bare Subsonic forms. */
|
||||
export function sameQueueTrackId(a: string | undefined | null, b: string | undefined | null): boolean {
|
||||
if (a == null || b == null) return false;
|
||||
const na = normalizeAnalysisTrackId(a) ?? a;
|
||||
const nb = normalizeAnalysisTrackId(b) ?? b;
|
||||
return na === nb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same-length + same-ids check. Used to skip no-op queue rewrites that would
|
||||
* otherwise reset selection / scroll / drag-source state in subscribers.
|
||||
*/
|
||||
export function queuesStructuralEqual(a: Track[], b: Track[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!sameQueueTrackId(a[i]?.id, b[i]?.id)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** One-level clone so callers can mutate per-track fields without aliasing state. */
|
||||
export function shallowCloneQueueTracks(queue: Track[]): Track[] {
|
||||
return queue.map(t => ({ ...t }));
|
||||
}
|
||||
Reference in New Issue
Block a user