mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
refactor(network): decouple network-reachability layer from features → lib/network
The connection-reachability + Subsonic network-guard helpers were pinned low by four lib/api consumers (subsonicLibrary/Playlists/Ratings/Scrobble) yet ran two lower-layer→feature inversions. Both removed without a registry: - devOfflineBrowseStore is a self-contained DEV-only toggle with zero offline- feature coupling — it was only colocated there. Relocated features/offline/store → store/ (global). The @/features/offline barrel re-exports it so feature/UI consumers are unchanged; the three lower-layer readers (subsonicNetworkGuard, activeServerReachability, useConnectionStatus) now import it from @/store directly. - subsonicNetworkGuard's only other feature dep was playback's resolvePlaybackUrl, used solely for the psysonic-local:// skip check. Added hasLocalPlaybackUrl to the existing M4 substrate store/localPlaybackResolve — it mirrors resolvePlaybackUrl's local-source branch exactly (same profile resolution; the empty-serverId playback fallback never applies in the guard), so the skip stays bit-identical. network/ (subsonicNetworkGuard + activeServerReachability + tests) → lib/network, now @/features-free. useConnectionStatus is now iron-rule-clean and stays in hooks/ (cross-cutting). Test mocks retargeted to the new seam modules. Behavior-adjacent (covered by suite; default paths identical): the local-bytes skip helper — flag for offline-playback runtime QA alongside the M4 media-resolver seam.
This commit is contained in:
@@ -21,7 +21,7 @@ import { libraryIsReady } from '@/lib/library/libraryReady';
|
||||
import {
|
||||
shouldAttemptSubsonicForActiveServer,
|
||||
shouldAttemptSubsonicForServer,
|
||||
} from '@/utils/network/subsonicNetworkGuard';
|
||||
} from '@/lib/network/subsonicNetworkGuard';
|
||||
|
||||
type AlbumPayload = ResolvedAlbum;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useOfflineJobStore } from '@/features/offline';
|
||||
import { isOfflinePinComplete } from '@/features/offline';
|
||||
import { dequeueOfflinePin } from '@/features/offline';
|
||||
import { reconcileLibraryTierForAlbum } from '@/features/offline';
|
||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||
import { shouldAttemptSubsonicForServer } from '@/lib/network/subsonicNetworkGuard';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { useZipDownloadStore } from '@/features/offline';
|
||||
import AlbumCard from '@/features/album/components/AlbumCard';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useAuthStore } from '@/store/authStore';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
import type { TopFavoriteArtist } from '@/features/favorites/components/TopFavoriteArtists';
|
||||
import { useConnectionStatus } from '@/hooks/useConnectionStatus';
|
||||
import { isActiveServerReachable } from '@/utils/network/activeServerReachability';
|
||||
import { isActiveServerReachable } from '@/lib/network/activeServerReachability';
|
||||
import { useOfflineBrowseContext } from '@/features/offline';
|
||||
import { useOfflineBrowseReloadToken } from '@/features/offline';
|
||||
import {
|
||||
|
||||
@@ -16,11 +16,11 @@ import type { SubsonicArtistInfo, SubsonicSong, SubsonicAlbum, SubsonicArtist }
|
||||
vi.mock('@/lib/api/subsonicArtists');
|
||||
vi.mock('@/lib/api/subsonicLibrary');
|
||||
vi.mock('@/api/bandsintown');
|
||||
vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
|
||||
vi.mock('@/lib/network/subsonicNetworkGuard', () => ({
|
||||
shouldAttemptSubsonicForServer: vi.fn(() => true),
|
||||
}));
|
||||
|
||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||
import { shouldAttemptSubsonicForServer } from '@/lib/network/subsonicNetworkGuard';
|
||||
import { getArtistForServer, getArtistInfoForServer, getTopSongsForServer } from '@/lib/api/subsonicArtists';
|
||||
import { getAlbumForServer, getSongForServer } from '@/lib/api/subsonicLibrary';
|
||||
import { fetchBandsintownEvents } from '@/api/bandsintown';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fetchBandsintownEvents, type BandsintownEvent } from '@/api/bandsintown
|
||||
import type { ArtistStats, TrackStats } from '@/music-network';
|
||||
import { getMusicNetworkRuntimeOrNull } from '@/music-network';
|
||||
import { makeCache } from '@/lib/cache/nowPlayingCache';
|
||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||
import { shouldAttemptSubsonicForServer } from '@/lib/network/subsonicNetworkGuard';
|
||||
import { useConnectionStatus } from '@/hooks/useConnectionStatus';
|
||||
|
||||
// Module-level TTL caches (shared across mounts)
|
||||
|
||||
@@ -13,10 +13,10 @@ import * as subsonicArtists from '@/lib/api/subsonicArtists';
|
||||
import * as subsonicLibrary from '@/lib/api/subsonicLibrary';
|
||||
|
||||
// Network reachability is decided by the guard; mock it so we can test both arms.
|
||||
vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
|
||||
vi.mock('@/lib/network/subsonicNetworkGuard', () => ({
|
||||
shouldAttemptSubsonicForServer: vi.fn(() => true),
|
||||
}));
|
||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||
import { shouldAttemptSubsonicForServer } from '@/lib/network/subsonicNetworkGuard';
|
||||
import {
|
||||
resolveNpAlbum,
|
||||
resolveNpDiscography,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { libraryGetTrack, libraryGetTracksByAlbum } from '@/lib/api/library';
|
||||
import { getArtistForServer, getTopSongsForServer } from '@/lib/api/subsonicArtists';
|
||||
import { getAlbumForServer, getSongForServer } from '@/lib/api/subsonicLibrary';
|
||||
import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes';
|
||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||
import { shouldAttemptSubsonicForServer } from '@/lib/network/subsonicNetworkGuard';
|
||||
import { loadAlbumFromLibraryIndex, loadArtistFromLibraryIndex } from '@/features/offline';
|
||||
import { trackToSong } from '@/lib/library/advancedSearchLocal';
|
||||
import { libraryIsReady } from '@/lib/library/libraryReady';
|
||||
|
||||
@@ -19,7 +19,7 @@ export * from './hooks/useOfflineBrowseContext';
|
||||
export * from './hooks/useOfflineBrowseReloadToken';
|
||||
export * from './hooks/useOfflineLibraryFilterSuspend';
|
||||
export * from './hooks/useZipDownloadBridge';
|
||||
export * from './store/devOfflineBrowseStore';
|
||||
export * from '@/store/devOfflineBrowseStore';
|
||||
export * from './store/downloadModalStore';
|
||||
export * from './store/favoritesOfflineSyncStore';
|
||||
export * from './store/offlineJobStore';
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
/** DEV-only: simulate full offline (no server probes, no Subsonic, local playback only). */
|
||||
interface DevOfflineBrowseState {
|
||||
forceOffline: boolean;
|
||||
setForceOffline: (v: boolean) => void;
|
||||
toggleForceOffline: () => void;
|
||||
}
|
||||
|
||||
export const useDevOfflineBrowseStore = create<DevOfflineBrowseState>()((set, get) => ({
|
||||
forceOffline: false,
|
||||
setForceOffline: (v) => set({ forceOffline: v }),
|
||||
toggleForceOffline: () => set({ forceOffline: !get().forceOffline }),
|
||||
}));
|
||||
|
||||
/** True when DEV mode forces disconnected server + offline player behavior. */
|
||||
export function isDevOfflineBrowseForced(): boolean {
|
||||
return import.meta.env.DEV && useDevOfflineBrowseStore.getState().forceOffline;
|
||||
}
|
||||
@@ -19,10 +19,10 @@ import { resolveAlbumForServer } from '@/features/offline/utils/offlineMediaReso
|
||||
|
||||
const isActiveServerReachableMock = vi.fn(() => true);
|
||||
const shouldAttemptSubsonicForServerMock = vi.fn((_serverId: string, _trackId?: string) => true);
|
||||
vi.mock('@/utils/network/activeServerReachability', () => ({
|
||||
vi.mock('@/lib/network/activeServerReachability', () => ({
|
||||
isActiveServerReachable: () => isActiveServerReachableMock(),
|
||||
}));
|
||||
vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
|
||||
vi.mock('@/lib/network/subsonicNetworkGuard', () => ({
|
||||
shouldAttemptSubsonicForServer: (serverId: string, trackId?: string) =>
|
||||
shouldAttemptSubsonicForServerMock(serverId, trackId),
|
||||
}));
|
||||
|
||||
@@ -16,7 +16,7 @@ const getStarredForServerMock = vi.fn(async (_serverId: string) => ({
|
||||
|
||||
const isActiveServerReachableMock = vi.fn(() => true);
|
||||
|
||||
vi.mock('@/utils/network/activeServerReachability', () => ({
|
||||
vi.mock('@/lib/network/activeServerReachability', () => ({
|
||||
isActiveServerReachable: () => isActiveServerReachableMock(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
|
||||
import { getMediaDir } from '@/lib/media/mediaDir';
|
||||
import { resolveIndexKey, serverIndexKeyForProfile } from '@/lib/server/serverIndexKey';
|
||||
import { FAVORITES_OFFLINE_JOB_ID } from '@/features/offline/utils/favoritesOfflineConstants';
|
||||
import { isActiveServerReachable } from '@/utils/network/activeServerReachability';
|
||||
import { isActiveServerReachable } from '@/lib/network/activeServerReachability';
|
||||
import { favoritesServerIds } from '@/features/offline/utils/favoritesOfflineBrowse';
|
||||
import { loadAlbumFromLibraryIndex } from '@/features/offline/utils/offlineLibraryIndexLoad';
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import { useDevOfflineBrowseStore } from '@/features/offline/store/devOfflineBrowseStore';
|
||||
import { resetActiveServerConnectionSnapshot } from '@/utils/network/activeServerReachability';
|
||||
import { useDevOfflineBrowseStore } from '@/store/devOfflineBrowseStore';
|
||||
import { resetActiveServerConnectionSnapshot } from '@/lib/network/activeServerReachability';
|
||||
import { useOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
|
||||
|
||||
describe('useOfflineBrowseActive', () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
isDevOfflineBrowseForced,
|
||||
useDevOfflineBrowseStore,
|
||||
} from '@/features/offline/store/devOfflineBrowseStore';
|
||||
} from '@/store/devOfflineBrowseStore';
|
||||
import { useConnectionStatus } from '@/hooks/useConnectionStatus';
|
||||
import { isActiveServerReachable } from '@/utils/network/activeServerReachability';
|
||||
import { isActiveServerReachable } from '@/lib/network/activeServerReachability';
|
||||
|
||||
/** True when browse/detail pages should use local-bytes-only data sources. */
|
||||
export function isOfflineBrowseActive(): boolean {
|
||||
|
||||
@@ -44,7 +44,7 @@ vi.mock('@/features/offline/utils/offlinePlaylistBrowse', () => ({
|
||||
loadOfflineBrowsablePlaylistMock(playlistId, serverId),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
|
||||
vi.mock('@/lib/network/subsonicNetworkGuard', () => ({
|
||||
shouldAttemptSubsonicForServer: (serverId: string, trackId?: string) =>
|
||||
shouldAttemptSubsonicForServerMock(serverId, trackId),
|
||||
}));
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
SubsonicSong,
|
||||
} from '@/lib/api/subsonicTypes';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||
import { shouldAttemptSubsonicForServer } from '@/lib/network/subsonicNetworkGuard';
|
||||
import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
|
||||
import { libraryIsReady } from '@/lib/library/libraryReady';
|
||||
import { registerMediaResolver } from '@/store/mediaResolver';
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
SubsonicArtist,
|
||||
SubsonicSong,
|
||||
} from '@/lib/api/subsonicTypes';
|
||||
import { isActiveServerReachable } from '@/utils/network/activeServerReachability';
|
||||
import { isActiveServerReachable } from '@/lib/network/activeServerReachability';
|
||||
import {
|
||||
albumToAlbum,
|
||||
trackToSong,
|
||||
|
||||
@@ -24,7 +24,7 @@ const isReachableMock = vi.fn(() => true);
|
||||
const enqueueMock = vi.fn((_task: unknown) => true);
|
||||
const invokeMock = vi.fn(async (_cmd: string, _args?: unknown) => ({}));
|
||||
|
||||
vi.mock('@/utils/network/activeServerReachability', () => ({
|
||||
vi.mock('@/lib/network/activeServerReachability', () => ({
|
||||
isActiveServerReachable: () => isReachableMock(),
|
||||
onActiveServerBecameReachable: () => () => {},
|
||||
}));
|
||||
|
||||
@@ -14,7 +14,7 @@ import { getMediaDir } from '@/lib/media/mediaDir';
|
||||
import {
|
||||
isActiveServerReachable,
|
||||
onActiveServerBecameReachable,
|
||||
} from '@/utils/network/activeServerReachability';
|
||||
} from '@/lib/network/activeServerReachability';
|
||||
import { resolveIndexKey, serverIndexKeyForProfile } from '@/lib/server/serverIndexKey';
|
||||
import { resolveServerIdForIndexKey } from '@/lib/server/serverLookup';
|
||||
import { findLocalPlaybackEntry } from '@/store/localPlaybackResolve';
|
||||
|
||||
@@ -8,7 +8,7 @@ const isOfflinePinCompleteMock = vi.fn((_albumId: string, _serverId: string) =>
|
||||
const resolveAlbumForServerMock = vi.fn();
|
||||
const downloadAlbumMock = vi.fn();
|
||||
|
||||
vi.mock('@/utils/network/activeServerReachability', () => ({
|
||||
vi.mock('@/lib/network/activeServerReachability', () => ({
|
||||
isActiveServerReachable: () => isActiveServerReachableMock(),
|
||||
onActiveServerBecameReachable: () => () => {},
|
||||
}));
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useAuthStore } from '@/store/authStore';
|
||||
import type { OfflineAlbumMeta } from '@/features/offline/store/offlineStore';
|
||||
import { useOfflineStore } from '@/features/offline/store/offlineStore';
|
||||
import { trackToSong } from '@/lib/library/advancedSearchLocal';
|
||||
import { isActiveServerReachable, onActiveServerBecameReachable } from '@/utils/network/activeServerReachability';
|
||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||
import { isActiveServerReachable, onActiveServerBecameReachable } from '@/lib/network/activeServerReachability';
|
||||
import { shouldAttemptSubsonicForServer } from '@/lib/network/subsonicNetworkGuard';
|
||||
import { resolveServerIdForIndexKey } from '@/lib/server/serverLookup';
|
||||
import { isOfflinePinComplete } from '@/features/offline/utils/offlineLibraryHelpers';
|
||||
import { resolveAlbumForServer } from '@/features/offline/utils/offlineMediaResolve';
|
||||
|
||||
@@ -37,7 +37,7 @@ vi.mock('@/lib/api/subsonicPlayQueue', () => ({
|
||||
savePlayQueue: vi.fn(async () => undefined),
|
||||
getPlayQueue: vi.fn(async () => ({ songs: [], current: undefined, position: 0 })),
|
||||
}));
|
||||
vi.mock('@/utils/network/activeServerReachability', () => ({
|
||||
vi.mock('@/lib/network/activeServerReachability', () => ({
|
||||
isActiveServerReachable: () => true,
|
||||
}));
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ vi.mock('@/lib/api/subsonicPlayQueue', () => ({
|
||||
savePlayQueue: vi.fn(async () => undefined),
|
||||
getPlayQueue: vi.fn(async () => ({ songs: [], current: undefined, position: 0 })),
|
||||
}));
|
||||
vi.mock('@/utils/network/activeServerReachability', () => ({
|
||||
vi.mock('@/lib/network/activeServerReachability', () => ({
|
||||
isActiveServerReachable: () => true,
|
||||
}));
|
||||
vi.mock('@/lib/api/subsonicStreamUrl', () => ({
|
||||
|
||||
@@ -13,7 +13,7 @@ const { savePlayQueueMock, playerState, progressSnapshot, isSubsonicServerReacha
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/api/subsonicPlayQueue', () => ({ savePlayQueue: savePlayQueueMock }));
|
||||
vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
|
||||
vi.mock('@/lib/network/subsonicNetworkGuard', () => ({
|
||||
isSubsonicServerReachable: (serverId: string) => isSubsonicServerReachableMock(serverId),
|
||||
}));
|
||||
vi.mock('@/features/playback/utils/playback/playbackServer', () => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { savePlayQueue } from '@/lib/api/subsonicPlayQueue';
|
||||
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
|
||||
import { isSubsonicServerReachable } from '@/utils/network/subsonicNetworkGuard';
|
||||
import { isSubsonicServerReachable } from '@/lib/network/subsonicNetworkGuard';
|
||||
import {
|
||||
filterQueueRefsForPlaybackServer,
|
||||
getPlaybackServerId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { getAlbumList } from '@/lib/api/subsonicLibrary';
|
||||
import { isActiveServerReachable } from '@/utils/network/activeServerReachability';
|
||||
import { isActiveServerReachable } from '@/lib/network/activeServerReachability';
|
||||
import {
|
||||
NEW_RELEASES_RESET_DELAY_MS,
|
||||
NEW_RELEASES_SEEN_MAX_IDS,
|
||||
|
||||
Reference in New Issue
Block a user