From 6651abbc6f10d8ccf49f6c2ea6864a4612b2aea0 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:45:34 +0200 Subject: [PATCH] =?UTF-8?q?refactor(decouple):=20playback-engine=20bridge?= =?UTF-8?q?=20=E2=80=94=20break=20authStore=E2=86=92engine=20edges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/app/MainApp.tsx | 1 + src/store/authAudioSettingsActions.ts | 16 +++++----- src/store/authServerProfileActions.ts | 5 ++- src/store/authStore.servers.test.ts | 1 + src/store/playbackEngineBridge.ts | 39 +++++++++++++++++++++++ src/store/playbackEngineBridgeRegister.ts | 13 ++++++++ 6 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 src/store/playbackEngineBridge.ts create mode 100644 src/store/playbackEngineBridgeRegister.ts diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx index 8419a86e..d42fb1b8 100644 --- a/src/app/MainApp.tsx +++ b/src/app/MainApp.tsx @@ -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'; diff --git a/src/store/authAudioSettingsActions.ts b/src/store/authAudioSettingsActions.ts index 53fe5204..b2bd99de 100644 --- a/src/store/authAudioSettingsActions.ts +++ b/src/store/authAudioSettingsActions.ts @@ -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 }), diff --git a/src/store/authServerProfileActions.ts b/src/store/authServerProfileActions.ts index 2b263e16..56c6f761 100644 --- a/src/store/authServerProfileActions.ts +++ b/src/store/authServerProfileActions.ts @@ -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(); } diff --git a/src/store/authStore.servers.test.ts b/src/store/authStore.servers.test.ts index f0116d37..235bee49 100644 --- a/src/store/authStore.servers.test.ts +++ b/src/store/authStore.servers.test.ts @@ -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'; diff --git a/src/store/playbackEngineBridge.ts b/src/store/playbackEngineBridge.ts new file mode 100644 index 00000000..c4ae900c --- /dev/null +++ b/src/store/playbackEngineBridge.ts @@ -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(); +} diff --git a/src/store/playbackEngineBridgeRegister.ts b/src/store/playbackEngineBridgeRegister.ts new file mode 100644 index 00000000..1a50a7f0 --- /dev/null +++ b/src/store/playbackEngineBridgeRegister.ts @@ -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(), +});