refactor(decouple): media-resolver seam — invert resolve* off the audio core

Add core seam store/mediaResolver.ts: a registry (registerMediaResolver) with
delegating resolveAlbum/resolveArtist/resolvePlaylist + the pure auth wrappers
resolveMediaServerId/resolveAlbumForActiveServer/resolveAlbumForServer and the
ResolvedAlbum type. Default (unregistered) = network-only safety net (no
library-index/offline branch — those stay in the feature).

features/offline/utils/offlineMediaResolve keeps the offline-aware policy impls
and registers them at module init (registerMediaResolver). The call runs at boot:
AppShell eagerly imports the @/features/offline barrel, whose export * evaluates
offlineMediaResolve. The 3 thin wrappers + ResolvedAlbum move to the core seam and
are re-exported from offlineMediaResolve so the offline barrel still surfaces them
for the ~25 UI consumers (unchanged).

Repointed the 5 audio-core call sites (fetchTracksForSource, playAlbum,
playArtistShuffled, playByOpaqueId, luckyMixHelpers) from @/features/offline to
@/store/mediaResolver. Migrated playAlbum.test's mock to the new module via
importOriginal-spread (keeps registerMediaResolver callable when the offline
barrel loads transitively — partial-mock-collapse otherwise).

Decouple Step 2b. The audio core's only remaining @/features/offline reference is
now the type-only localPlaybackMigration import (erased). Runtime offline edge gone;
orbit seam (Step 3) remains.

tsc 0, lint 0, full suite 319/2353 green.
This commit is contained in:
Psychotoxical
2026-06-30 14:08:43 +02:00
parent bd5143d98c
commit 42ef09f1e4
8 changed files with 112 additions and 24 deletions
@@ -11,6 +11,7 @@ import { useAuthStore } from '@/store/authStore';
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
import { libraryIsReady } from '@/utils/library/libraryReady';
import { registerMediaResolver } from '@/store/mediaResolver';
import {
loadAlbumFromLibraryIndex,
loadArtistFromLibraryIndex,
@@ -25,7 +26,16 @@ import {
playlistsOfflineBrowseEnabled,
} from '@/features/offline/utils/offlinePlaylistBrowse';
export type ResolvedAlbum = { album: SubsonicAlbum; songs: SubsonicSong[] };
// resolveAlbumForServer / resolveMediaServerId / resolveAlbumForActiveServer +
// the ResolvedAlbum type now live in the core seam; re-exported so the
// @/features/offline barrel keeps surfacing them for UI consumers.
import type { ResolvedAlbum } from '@/store/mediaResolver';
export type { ResolvedAlbum };
export {
resolveAlbumForServer,
resolveMediaServerId,
resolveAlbumForActiveServer,
} from '@/store/mediaResolver';
/**
* Album detail / play / enqueue: the local SQLite index first when it is ready
@@ -67,9 +77,6 @@ export async function resolveAlbum(
}
}
/** @deprecated Use {@link resolveAlbum}. */
export const resolveAlbumForServer = resolveAlbum;
export async function resolveArtist(
serverId: string,
artistId: string,
@@ -115,16 +122,7 @@ export async function resolvePlaylist(
}
}
export function resolveMediaServerId(explicit?: string | null): string | null {
return explicit ?? useAuthStore.getState().activeServerId;
}
/** Resolve album for active server when `serverId` omitted. */
export async function resolveAlbumForActiveServer(
albumId: string,
serverId?: string,
): Promise<ResolvedAlbum | null> {
const sid = serverId ?? useAuthStore.getState().activeServerId;
if (!sid) return null;
return resolveAlbum(sid, albumId);
}
// Install the offline-aware policy into the core seam. Runs at module init,
// which happens at boot because AppShell eagerly imports the @/features/offline
// barrel (export * evaluates this module).
registerMediaResolver({ resolveAlbum, resolveArtist, resolvePlaylist });