Compare commits

2 Commits

Author SHA1 Message Date
Senko-san 89cf66f28a fix(api): refetch from server when online (stale-while-revalidate)
Docker Build & Publish / build (push) Successful in 35s
Docker Build & Publish / push (push) Failing after 2s
Docker Build & Publish / Prune old image versions (push) Has been skipped
The Tier-2 rehydrated cache seeded fulfilled entries at startup, so RTKQ served stale data and never hit the network (manual metadata edits, etc. only appeared after clearing the cache). Enable refetchOnMountOrArgChange + refetchOnReconnect + refetchOnFocus and wire setupListeners, so the cached snapshot still shows instantly but the server silently revalidates it whenever reachable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:13:59 +03:00
Senko-san f5a6b919aa fix(library): poll track list while enrichment is pending
The library tracks query wasn't refetching, so a track stayed on "Identifying metadata…" until an unrelated Track-tag invalidation. Poll every 4s while any listed track is metadata_status=pending, then stop (and never while offline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:09:47 +03:00
3 changed files with 29 additions and 1 deletions
+9
View File
@@ -5,6 +5,15 @@ import { REHYDRATE_API, type RehydrateApiPayload } from './rehydrate';
export const api = createApi({ export const api = createApi({
reducerPath: 'api', reducerPath: 'api',
baseQuery: baseQueryWithReauth, baseQuery: baseQueryWithReauth,
// Stale-while-revalidate. The Tier-2 rehydrated cache (below) seeds fulfilled
// entries at startup, which would otherwise make RTKQ serve stale data and
// never hit the network. These flags keep showing the cached snapshot
// instantly but silently refetch from the server whenever it's reachable —
// on mount/arg change, on reconnect, and on window refocus. The result: the
// server is the source of truth when online; the cache is only a fallback.
refetchOnMountOrArgChange: true,
refetchOnReconnect: true,
refetchOnFocus: true,
tagTypes: [ tagTypes: [
'Track', 'Track',
'Album', 'Album',
+15 -1
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@@ -54,8 +54,14 @@ export function LibraryPage() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch] = useDebounce(search, 300); const [debouncedSearch] = useDebounce(search, 300);
// Poll while any listed track is still being enriched, then stop. Enrichment
// runs asynchronously in a worker after import/upload; without this the row
// stays stuck on "Identifying metadata…" until something else invalidates the
// Track tag. Cleared to 0 once nothing is pending (and while offline).
const [tracksPollMs, setTracksPollMs] = useState(0);
const tracksQuery = useGetTracksQuery( const tracksQuery = useGetTracksQuery(
debouncedSearch ? { search } : undefined, debouncedSearch ? { search } : undefined,
{ pollingInterval: tracksPollMs },
); );
const albumsQuery = useGetAlbumsQuery( const albumsQuery = useGetAlbumsQuery(
debouncedSearch ? { search } : undefined, debouncedSearch ? { search } : undefined,
@@ -73,6 +79,14 @@ export function LibraryPage() {
const localArtists = useAppSelector(selectLocalArtists); const localArtists = useAppSelector(selectLocalArtists);
const q = debouncedSearch.trim().toLowerCase(); const q = debouncedSearch.trim().toLowerCase();
const anyPending =
!offline &&
(tracksQuery.data?.items.some((tr) => tr.metadataStatus === 'pending') ??
false);
useEffect(() => {
setTracksPollMs(anyPending ? 4000 : 0);
}, [anyPending]);
// Live server data wins; offline we fall back to the locally-composed list. // Live server data wins; offline we fall back to the locally-composed list.
const tracksToShow = const tracksToShow =
tracksQuery.data?.items ?? tracksQuery.data?.items ??
+5
View File
@@ -1,4 +1,5 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { api } from '../api'; import { api } from '../api';
import authReducer from './slices/auth'; import authReducer from './slices/auth';
import connectionReducer from './slices/connection'; import connectionReducer from './slices/connection';
@@ -27,6 +28,10 @@ export const store = configureStore({
getDefaultMiddleware().concat(api.middleware), getDefaultMiddleware().concat(api.middleware),
}); });
// Enable refetchOnReconnect / refetchOnFocus by dispatching the browser's
// online + focus events into RTKQ (no-op without the api flags set in api/index).
setupListeners(store.dispatch);
// Flush queue/player changes back to localStorage (throttled). // Flush queue/player changes back to localStorage (throttled).
startPersistence(store); startPersistence(store);