mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(player): E.7 — extract two small file-private helpers (#570)
`emitNormalizationDebug` (debug-mode trace forwarder, 15+ internal call sites) and `isInOrbitSession` (Orbit-active guard used by next() and the async fallback paths to suppress local queue extensions, 5 call sites) move into dedicated modules under src/store/. Both were file-private — no caller-side changes outside playerStore's own imports. playerStore 3434 → 3415 LOC.
This commit is contained in:
committed by
GitHub
parent
81b161a418
commit
a1d7cf330d
@@ -0,0 +1,51 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const { authState, invokeMock } = vi.hoisted(() => ({
|
||||||
|
authState: { loggingMode: 'off' as 'off' | 'debug' | string },
|
||||||
|
invokeMock: vi.fn(async (_cmd: string, _args?: Record<string, unknown>) => undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@tauri-apps/api/core', () => ({ invoke: invokeMock }));
|
||||||
|
vi.mock('./authStore', () => ({ useAuthStore: { getState: () => authState } }));
|
||||||
|
|
||||||
|
import { emitNormalizationDebug } from './normalizationDebug';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
authState.loggingMode = 'off';
|
||||||
|
invokeMock.mockClear();
|
||||||
|
invokeMock.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('emitNormalizationDebug', () => {
|
||||||
|
it('is a no-op when logging mode is not debug', () => {
|
||||||
|
emitNormalizationDebug('refresh:start', { trackId: 't1' });
|
||||||
|
expect(invokeMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards a JSON payload to frontend_debug_log in debug mode', () => {
|
||||||
|
authState.loggingMode = 'debug';
|
||||||
|
emitNormalizationDebug('refresh:start', { trackId: 't1' });
|
||||||
|
expect(invokeMock).toHaveBeenCalledTimes(1);
|
||||||
|
const [cmd, args] = invokeMock.mock.calls[0];
|
||||||
|
expect(cmd).toBe('frontend_debug_log');
|
||||||
|
expect(args).toMatchObject({
|
||||||
|
scope: 'normalization',
|
||||||
|
message: JSON.stringify({ step: 'refresh:start', details: { trackId: 't1' } }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes calls without details too', () => {
|
||||||
|
authState.loggingMode = 'debug';
|
||||||
|
emitNormalizationDebug('plain-step');
|
||||||
|
const args = invokeMock.mock.calls[0][1] as { message: string };
|
||||||
|
expect(JSON.parse(args.message)).toEqual({ step: 'plain-step' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('swallows invoke rejections (best-effort instrumentation)', async () => {
|
||||||
|
authState.loggingMode = 'debug';
|
||||||
|
invokeMock.mockRejectedValueOnce(new Error('rust busy'));
|
||||||
|
expect(() => emitNormalizationDebug('refresh:start')).not.toThrow();
|
||||||
|
// Give the rejected promise a tick to settle without throwing.
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { useAuthStore } from './authStore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forward a structured normalization-pipeline trace to the Rust-side debug
|
||||||
|
* log file when Settings → Logging is set to **Debug**. A no-op otherwise,
|
||||||
|
* so the dozens of call sites in `playerStore` (refresh / backfill / engine
|
||||||
|
* sync / track-switch instrumentation) carry zero cost in normal mode.
|
||||||
|
*
|
||||||
|
* Errors invoking the Rust command are swallowed — this is best-effort
|
||||||
|
* instrumentation, not a playback dependency.
|
||||||
|
*/
|
||||||
|
export function emitNormalizationDebug(step: string, details?: Record<string, unknown>): void {
|
||||||
|
if (useAuthStore.getState().loggingMode !== 'debug') return;
|
||||||
|
void invoke('frontend_debug_log', {
|
||||||
|
scope: 'normalization',
|
||||||
|
message: JSON.stringify({ step, details }),
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const { orbitState } = vi.hoisted(() => ({
|
||||||
|
orbitState: {
|
||||||
|
role: null as 'host' | 'guest' | null,
|
||||||
|
phase: 'idle' as 'idle' | 'starting' | 'joining' | 'active' | 'ending' | 'ended' | 'error',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./orbitStore', () => ({
|
||||||
|
useOrbitStore: { getState: () => orbitState },
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { isInOrbitSession } from './orbitSession';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
orbitState.role = null;
|
||||||
|
orbitState.phase = 'idle';
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isInOrbitSession', () => {
|
||||||
|
it('returns false when role is null', () => {
|
||||||
|
expect(isInOrbitSession()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['active', 'joining', 'starting'] as const)(
|
||||||
|
"returns true for role='host', phase='%s'",
|
||||||
|
phase => {
|
||||||
|
orbitState.role = 'host';
|
||||||
|
orbitState.phase = phase;
|
||||||
|
expect(isInOrbitSession()).toBe(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(['active', 'joining', 'starting'] as const)(
|
||||||
|
"returns true for role='guest', phase='%s'",
|
||||||
|
phase => {
|
||||||
|
orbitState.role = 'guest';
|
||||||
|
orbitState.phase = phase;
|
||||||
|
expect(isInOrbitSession()).toBe(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each(['idle', 'ending', 'ended', 'error'] as const)(
|
||||||
|
"returns false for transient/inactive phase='%s'",
|
||||||
|
phase => {
|
||||||
|
orbitState.role = 'host';
|
||||||
|
orbitState.phase = phase;
|
||||||
|
expect(isInOrbitSession()).toBe(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useOrbitStore } from './orbitStore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the user is part of an Orbit session (any role, any phase short
|
||||||
|
* of `idle` / `error` / `ended`). Used by `next()` and its async fallback
|
||||||
|
* callbacks to suppress local queue-extension paths (radio top-up, infinite
|
||||||
|
* queue, queue-exhausted refill) — those would either pop the
|
||||||
|
* `orbitBulkGuard` modal or silently inject tracks the host didn't pick.
|
||||||
|
* Also called inside in-flight `.then()` callbacks so a fetch scheduled
|
||||||
|
* just before the user joined Orbit doesn't fire a `playTrack` after the
|
||||||
|
* join.
|
||||||
|
*/
|
||||||
|
export function isInOrbitSession(): boolean {
|
||||||
|
const o = useOrbitStore.getState();
|
||||||
|
if (o.role !== 'host' && o.role !== 'guest') return false;
|
||||||
|
return o.phase === 'active' || o.phase === 'joining' || o.phase === 'starting';
|
||||||
|
}
|
||||||
@@ -45,6 +45,8 @@ import {
|
|||||||
shouldRebindPlaybackToHotCache,
|
shouldRebindPlaybackToHotCache,
|
||||||
} from './playbackUrlRouting';
|
} from './playbackUrlRouting';
|
||||||
import { deriveNormalizationSnapshot } from './normalizationSnapshot';
|
import { deriveNormalizationSnapshot } from './normalizationSnapshot';
|
||||||
|
import { emitNormalizationDebug } from './normalizationDebug';
|
||||||
|
import { isInOrbitSession } from './orbitSession';
|
||||||
|
|
||||||
// Re-export the playback-progress public surface so existing call sites
|
// Re-export the playback-progress public surface so existing call sites
|
||||||
// (PlayerBar, FullscreenPlayer, WaveformSeek, LyricsPane, MobilePlayerView,
|
// (PlayerBar, FullscreenPlayer, WaveformSeek, LyricsPane, MobilePlayerView,
|
||||||
@@ -323,19 +325,6 @@ let infiniteQueueFetching = false;
|
|||||||
// Guard against concurrent radio top-up fetches.
|
// Guard against concurrent radio top-up fetches.
|
||||||
let radioFetching = false;
|
let radioFetching = false;
|
||||||
|
|
||||||
/** True when the user is part of an Orbit session (any role, any phase
|
|
||||||
* short of `idle` / `error` / `ended`). Used by `next()` and its async
|
|
||||||
* fallback callbacks to suppress local queue-extension paths (radio
|
|
||||||
* top-up, infinite-queue, queue-exhausted refill) — those would either
|
|
||||||
* pop the orbitBulkGuard modal or silently inject tracks the host
|
|
||||||
* didn't pick. The helper is also called inside in-flight `.then()`
|
|
||||||
* callbacks so a fetch scheduled just before the user joined Orbit
|
|
||||||
* doesn't fire a `playTrack` after the join. */
|
|
||||||
function isInOrbitSession(): boolean {
|
|
||||||
const o = useOrbitStore.getState();
|
|
||||||
if (o.role !== 'host' && o.role !== 'guest') return false;
|
|
||||||
return o.phase === 'active' || o.phase === 'joining' || o.phase === 'starting';
|
|
||||||
}
|
|
||||||
// Artist ID used to start the current radio session — persists across track
|
// Artist ID used to start the current radio session — persists across track
|
||||||
// advances so proactive loading works even when songs lack artistId.
|
// advances so proactive loading works even when songs lack artistId.
|
||||||
let currentRadioArtistId: string | null = null;
|
let currentRadioArtistId: string | null = null;
|
||||||
@@ -425,14 +414,6 @@ function queueUndoRestoreAudioEngine(opts: {
|
|||||||
touchHotCacheOnPlayback(track.id, authState.activeServerId ?? '');
|
touchHotCacheOnPlayback(track.id, authState.activeServerId ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitNormalizationDebug(step: string, details?: Record<string, unknown>) {
|
|
||||||
if (useAuthStore.getState().loggingMode !== 'debug') return;
|
|
||||||
void invoke('frontend_debug_log', {
|
|
||||||
scope: 'normalization',
|
|
||||||
message: JSON.stringify({ step, details }),
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce timer for seek slider drags.
|
// Debounce timer for seek slider drags.
|
||||||
let seekDebounce: ReturnType<typeof setTimeout> | null = null;
|
let seekDebounce: ReturnType<typeof setTimeout> | null = null;
|
||||||
// Target time of the last seek — blocks stale Rust progress ticks until the
|
// Target time of the last seek — blocks stale Rust progress ticks until the
|
||||||
|
|||||||
Reference in New Issue
Block a user