feat: poll in-flight requests on native + refresh after submit (#369)
test-web / test (push) Successful in 33s
android / Build + lint + test (push) Successful in 3m37s

Native RequestsViewModel gains poll-while-approved (silent reloads, paused
when nothing in-flight) for parity with the web auto-poll, and the SSE
collector now reloads silently instead of flashing the loading spinner.
Web discover submit invalidates qk.myRequests() so a new request appears
on /requests immediately.
This commit is contained in:
2026-06-06 23:18:44 -04:00
parent c8b21aa76e
commit 8f29cc7414
3 changed files with 83 additions and 1 deletions
+6
View File
@@ -2,6 +2,8 @@
import { pageTitle } from '$lib/branding';
import { createLidarrSearchQuery } from '$lib/api/lidarr';
import { createRequest } from '$lib/api/requests';
import { qk } from '$lib/api/queries';
import { useQueryClient } from '@tanstack/svelte-query';
import DiscoverResultCard from '$lib/components/DiscoverResultCard.svelte';
import DiscoverTabs from '$lib/components/DiscoverTabs.svelte';
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
@@ -12,6 +14,8 @@
LidarrSearchResult
} from '$lib/api/types';
const queryClient = useQueryClient();
// Local input is the source of truth for the discover query — Shell's global
// SearchInput targets /search, not /discover. We debounce by 250ms before
// pushing into `debouncedQ`, which is what the query factory observes.
@@ -87,6 +91,8 @@
const next = new Set(optimisticRequested);
next.add(r.mbid);
optimisticRequested = next;
// Surface the new request on /requests right away (+ start its poll) (#369).
queryClient.invalidateQueries({ queryKey: qk.myRequests() });
} catch {
// Swallow for v1: error toasts land in a later UX pass. The card stays
// in 'requestable' so the user can retry.
+30
View File
@@ -31,6 +31,15 @@ vi.mock('$lib/api/requests', () => ({
vi.mock('$lib/api/client', () => apiClientMock());
// The page calls useQueryClient() to invalidate qk.myRequests() after a
// submit (#369); stub it so rendering doesn't require a real QueryClient and
// we can assert the invalidation fires.
const queryClientMock = vi.hoisted(() => ({ invalidateQueries: vi.fn() }));
vi.mock('@tanstack/svelte-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/svelte-query')>();
return { ...actual, useQueryClient: () => queryClientMock };
});
import DiscoverPage from './+page.svelte';
import { createLidarrSearchQuery } from '$lib/api/lidarr';
import { createSuggestionsQuery } from '$lib/api/suggestions';
@@ -187,6 +196,27 @@ describe('Discover page', () => {
});
});
test('submitting a request invalidates myRequests so /requests updates (#369)', async () => {
vi.useFakeTimers();
const r = result({ mbid: 'art-mbid', artist_mbid: 'art-mbid', name: 'Boards of Canada' });
mockedCreateQuery.mockReturnValue(mockQuery<LidarrSearchResult[]>({ data: [r] }));
render(DiscoverPage);
const input = screen.getByLabelText(/search lidarr/i);
await fireEvent.input(input, { target: { value: 'boards' } });
await vi.advanceTimersByTimeAsync(250);
vi.useRealTimers();
const requestBtn = await screen.findByRole('button', {
name: /request boards of canada/i
});
await fireEvent.click(requestBtn);
await waitFor(() => {
expect(queryClientMock.invalidateQueries).toHaveBeenCalledWith({
queryKey: ['myRequests']
});
});
});
test('track-kind Request click opens confirm modal; Confirm calls createRequest', async () => {
vi.useFakeTimers();
const r = result({