refactor(decouple): playback-engine bridge — break authStore→engine edges

The last core→engine inversions blocking the playback-core move live in the
authStore settings/profile family: authServerProfileActions reads
usePlayerStore.queueServerId + calls clearQueueServerForPlayback (on server
delete), and authAudioSettingsActions calls
usePlayerStore.updateReplayGainForCurrentTrack from ~8 ReplayGain/normalization
setters. Both are re-exported through the authStore barrel, so a global store
would depend on the (soon-to-move) engine.

Add core seam store/playbackEngineBridge.ts: a registry exposing getQueueServerId
/ clearQueueServerForPlayback / updateReplayGainForCurrentTrack with no-op/null
defaults. The engine registers its impls via store/playbackEngineBridgeRegister.ts
(side-effect-imported by MainApp at boot). authStore actions now call the neutral
delegators instead of importing the engine.

Default is safe: the only callers are user-triggered settings/profile actions that
fire long after boot (engine already registered); at boot there's no current track
or queue binding, so the no-op would be correct anyway. authStore.servers.test
imports the register module so removeServer's queue-clear runs through the real
engine wiring.

Prepares the playback-core move (Engine → features/playback): with this + keeping
the 3 config-helper utils in utils/ + tolerating 2 type-only edges, the engine has
no inbound core→feature inversions left. tsc 0, lint 0, suite 319/2353 green.
Behavior-touching (server-delete queue clear, settings→gain refresh) → Frank QA.
This commit is contained in:
Psychotoxical
2026-06-30 14:45:34 +02:00
parent 651a2adba4
commit 6651abbc6f
6 changed files with 64 additions and 11 deletions
+1
View File
@@ -1,4 +1,5 @@
import { initAudioListeners } from '../store/initAudioListeners';
import '../store/playbackEngineBridgeRegister'; // installs the playback-engine bridge at boot
import { lazy, Suspense, useEffect, useState } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { invoke } from '@tauri-apps/api/core';
+8 -8
View File
@@ -4,7 +4,7 @@ import {
sanitizeAutodjOverlapCapSec,
} from '../utils/playback/autodjOverlapCap';
import { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB } from './authStoreDefaults';
import { usePlayerStore } from './playerStore';
import { updateReplayGainForCurrentTrack } from './playbackEngineBridge';
import type { AuthState } from './authStoreTypes';
type SetState = (
@@ -43,15 +43,15 @@ export function createAudioSettingsActions(set: SetState): Pick<
return {
setReplayGainEnabled: (v) => {
set({ replayGainEnabled: v });
usePlayerStore.getState().updateReplayGainForCurrentTrack();
updateReplayGainForCurrentTrack();
},
setNormalizationEngine: (v) => {
set({ normalizationEngine: v });
usePlayerStore.getState().updateReplayGainForCurrentTrack();
updateReplayGainForCurrentTrack();
},
setLoudnessTargetLufs: (v) => {
set({ loudnessTargetLufs: v });
usePlayerStore.getState().updateReplayGainForCurrentTrack();
updateReplayGainForCurrentTrack();
},
setLoudnessPreAnalysisAttenuationDb: (v) => {
const n = typeof v === 'number' ? v : Number(v);
@@ -60,19 +60,19 @@ export function createAudioSettingsActions(set: SetState): Pick<
},
resetLoudnessPreAnalysisAttenuationDbDefault: () => {
set({ loudnessPreAnalysisAttenuationDb: DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB });
usePlayerStore.getState().updateReplayGainForCurrentTrack();
updateReplayGainForCurrentTrack();
},
setReplayGainMode: (v) => {
set({ replayGainMode: v });
usePlayerStore.getState().updateReplayGainForCurrentTrack();
updateReplayGainForCurrentTrack();
},
setReplayGainPreGainDb: (v) => {
set({ replayGainPreGainDb: v });
usePlayerStore.getState().updateReplayGainForCurrentTrack();
updateReplayGainForCurrentTrack();
},
setReplayGainFallbackDb: (v) => {
set({ replayGainFallbackDb: v });
usePlayerStore.getState().updateReplayGainForCurrentTrack();
updateReplayGainForCurrentTrack();
},
setCrossfadeEnabled: (v) => set({ crossfadeEnabled: v }),
setCrossfadeSecs: (v) => set({ crossfadeSecs: v }),
+2 -3
View File
@@ -1,7 +1,6 @@
import type { AuthState } from './authStoreTypes';
import { generateId } from './authStoreHelpers';
import { usePlayerStore } from './playerStore';
import { clearQueueServerForPlayback } from '../utils/playback/playbackServer';
import { getQueueServerId, clearQueueServerForPlayback } from './playbackEngineBridge';
import { resolveServerIdForIndexKey } from '../utils/server/serverLookup';
type SetState = (
@@ -44,7 +43,7 @@ export function createServerProfileActions(set: SetState): Pick<
// queueServerId is the canonical index key (B1); resolve the
// canonical id back to a server UUID before comparing so a profile
// delete still clears the matching queue binding.
const queueSid = usePlayerStore.getState().queueServerId;
const queueSid = getQueueServerId();
if (queueSid && resolveServerIdForIndexKey(queueSid) === id) {
clearQueueServerForPlayback();
}
+1
View File
@@ -13,6 +13,7 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { useAuthStore } from './authStore';
import { usePlayerStore } from './playerStore';
import './playbackEngineBridgeRegister'; // wire removeServer's queue-clear through the real engine bridge
import { resetAuthStore } from '@/test/helpers/storeReset';
import { resetPlayerStore } from '@/test/helpers/storeReset';
+39
View File
@@ -0,0 +1,39 @@
// Playback-engine bridge. Global settings/profile stores (authStore family) need
// to trigger a few engine effects — clear a deleted server's queue binding, catch
// the current track up to a changed ReplayGain/normalization mode — but those
// stores are core and must not import the playback engine (iron rule; the engine
// will live in @/features/playback). The engine registers its operations here at
// boot (playbackEngineBridgeRegister.ts, side-effect-imported by MainApp); core
// callers invoke these neutral delegators.
//
// Default (unregistered) = no-op / null. Safe: the only callers are user-triggered
// settings/profile actions that fire long after boot, by which point the engine
// (loaded at app start) has registered. At boot there is no current track or queue
// binding, so the no-op default would be correct even if it were ever reached.
export interface PlaybackEngineBridge {
/** Active queue's bound server id, or null when nothing is bound. */
getQueueServerId(): string | null;
/** Drop the queue's server binding (used when its server profile is deleted). */
clearQueueServerForPlayback(): void;
/** Re-apply ReplayGain/normalization to the currently playing track. */
updateReplayGainForCurrentTrack(): void;
}
let bridge: PlaybackEngineBridge | null = null;
export function registerPlaybackEngineBridge(impl: PlaybackEngineBridge): void {
bridge = impl;
}
export function getQueueServerId(): string | null {
return bridge ? bridge.getQueueServerId() : null;
}
export function clearQueueServerForPlayback(): void {
bridge?.clearQueueServerForPlayback();
}
export function updateReplayGainForCurrentTrack(): void {
bridge?.updateReplayGainForCurrentTrack();
}
+13
View File
@@ -0,0 +1,13 @@
// Engine-side registration for the playback-engine bridge. Side-effect module:
// importing it installs the engine's operations into @/store/playbackEngineBridge.
// MainApp side-effect-imports this at boot. Lives with the engine (moves into
// @/features/playback alongside playerStore); the bridge itself stays in core.
import { usePlayerStore } from './playerStore';
import { clearQueueServerForPlayback } from '../utils/playback/playbackServer';
import { registerPlaybackEngineBridge } from './playbackEngineBridge';
registerPlaybackEngineBridge({
getQueueServerId: () => usePlayerStore.getState().queueServerId,
clearQueueServerForPlayback,
updateReplayGainForCurrentTrack: () => usePlayerStore.getState().updateReplayGainForCurrentTrack(),
});