refactor(decouple): orbit seam — invert orbit off the audio core

Add core seam store/orbitRuntime.ts: a registry (registerOrbitRuntime) exposing a
neutral session snapshot {role,phase,state} + an async bulkGuard, plus pure
derivations mirrored from the feature (isInOrbitSession, isOrbitPlaybackSyncActive,
estimateLivePosition). Default (unregistered) = neutral snapshot + bulkGuard allow
— bit-identical to today's no-session behavior, and a session can only start via
the topbar which loads the @/features/orbit barrel (→ registers) first.

features/orbit/utils/orbitBulkGuard registers the runtime at module init
(store-backed getSnapshot + the existing confirm-modal orbitBulkGuard as the gate).
The orbit feature keeps its own copies of the pure helpers for UI (incl. the
arg-form isOrbitPlaybackSyncActive(role,phase) used by two settings/player-bar
components), so nothing UI-facing changes.

Repointed the 9 audio-core sites (playbackRateStore, previewStore,
playbackReportSession, nextAction, resumeAction, playTrackAction,
queueMutationActions, playAlbum) from @/features/orbit to @/store/orbitRuntime;
state reads (useOrbitStore.getState().role/.state) become orbitSnapshot().
Migrated 6 audio-core test mocks to @/store/orbitRuntime via importOriginal-spread
(keeps registerOrbitRuntime callable). enqueueShareSearchPayload stays on the orbit
barrel (share util, not audio core) — its test mock unchanged.

Decouple Step 3 — last seam. The audio ENGINE is now free of @/features/* runtime
imports (only type-only edges + the non-engine composerBrowseSessionStore browse
store remain). Unblocks the playback-core move + utils/library→lib.

tsc 0, lint 0, full suite 319/2353 green.
NEEDS Frank's live-session QA before relying on it (host+guest bulk-replace modal,
guest catch-up, rate/preview/scrobble suppression).
This commit is contained in:
Psychotoxical
2026-06-30 14:19:22 +02:00
parent 42ef09f1e4
commit 651a2adba4
16 changed files with 110 additions and 22 deletions
+14 -2
View File
@@ -1,6 +1,7 @@
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { useConfirmModalStore } from '@/store/confirmModalStore';
import i18n from '@/lib/i18n';
import { registerOrbitRuntime } from '@/store/orbitRuntime';
/**
* Ask the user before dropping many tracks into the shared Orbit queue.
@@ -9,8 +10,8 @@ import i18n from '@/lib/i18n';
* when the user accepted the confirm dialog. Returns `false` only when an
* active-Orbit user explicitly cancelled.
*
* Lives in its own module so `playerStore` can use it without pulling the
* full `utils/orbit.ts` (which itself imports `playerStore` — circular).
* The audio core reaches this (and the orbit session snapshot) through the
* neutral `@/store/orbitRuntime` seam, not by importing the orbit feature.
*/
export async function orbitBulkGuard(count: number): Promise<boolean> {
const role = useOrbitStore.getState().role;
@@ -24,3 +25,14 @@ export async function orbitBulkGuard(count: number): Promise<boolean> {
cancelLabel: i18n.t('orbit.bulkConfirmNo'),
});
}
// Install the orbit runtime into the core seam at module init. The
// @/features/orbit barrel re-exports this module, so the topbar's barrel import
// evaluates it at boot — before any Orbit session can start.
registerOrbitRuntime({
getSnapshot: () => {
const o = useOrbitStore.getState();
return { role: o.role, phase: o.phase, state: o.state };
},
bulkGuard: orbitBulkGuard,
});