861dd8ef17
Half of #369 — the auto-poll piece. Inbox deferred (separate task). Both /requests (user) and /admin/requests (admin, 'approved' tab only) now refetch every 12s while at least one row has status='approved'. Stops automatically when all rows settle to pending/completed/rejected. TanStack Query's default refetchIntervalInBackground=false handles the visibility behavior — polling pauses when the tab is hidden and resumes on focus. Predicate hasInFlightRequest() is exported + tested so the polling logic is auditable independent of TanStack Query's internals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
import { createQuery } from '@tanstack/svelte-query';
|
|
import { api, apiFetch } from './client';
|
|
import { qk } from './queries';
|
|
import type { LidarrRequest, LidarrRequestKind } from './types';
|
|
|
|
/** Cadence for in-flight request polling. Tuned to match the operator's
|
|
"fast enough to feel live, slow enough to not hammer the server"
|
|
threshold from #369. */
|
|
const POLL_INTERVAL_MS = 12_000;
|
|
|
|
/** True if any row is mid-ingest (status === 'approved'). The status flips
|
|
to 'completed' / 'rejected' when Lidarr finishes, so this predicate
|
|
lets the page poll exactly while there's something interesting to watch. */
|
|
export function hasInFlightRequest(rows: readonly LidarrRequest[] | undefined): boolean {
|
|
return rows?.some((r) => r.status === 'approved') ?? false;
|
|
}
|
|
|
|
// CreateRequestParams is the shape callers pass; unused MBID fields are
|
|
// optional, and we omit them from the wire body rather than sending empty
|
|
// strings (the backend tolerates either, but this keeps test expectations
|
|
// crisp and matches the spec §5 wire shape).
|
|
export type CreateRequestParams = {
|
|
kind: LidarrRequestKind;
|
|
lidarr_artist_mbid: string;
|
|
lidarr_album_mbid?: string;
|
|
lidarr_track_mbid?: string;
|
|
artist_name: string;
|
|
album_title?: string;
|
|
track_title?: string;
|
|
};
|
|
|
|
type CreateRequestBody = {
|
|
kind: LidarrRequestKind;
|
|
lidarr_artist_mbid: string;
|
|
artist_name: string;
|
|
lidarr_album_mbid?: string;
|
|
lidarr_track_mbid?: string;
|
|
album_title?: string;
|
|
track_title?: string;
|
|
};
|
|
|
|
function buildCreateBody(params: CreateRequestParams): CreateRequestBody {
|
|
const body: CreateRequestBody = {
|
|
kind: params.kind,
|
|
lidarr_artist_mbid: params.lidarr_artist_mbid,
|
|
artist_name: params.artist_name
|
|
};
|
|
if (params.lidarr_album_mbid) body.lidarr_album_mbid = params.lidarr_album_mbid;
|
|
if (params.lidarr_track_mbid) body.lidarr_track_mbid = params.lidarr_track_mbid;
|
|
if (params.album_title) body.album_title = params.album_title;
|
|
if (params.track_title) body.track_title = params.track_title;
|
|
return body;
|
|
}
|
|
|
|
export async function createRequest(params: CreateRequestParams): Promise<LidarrRequest> {
|
|
return api.post<LidarrRequest>('/api/requests', buildCreateBody(params));
|
|
}
|
|
|
|
export async function listMyRequests(): Promise<LidarrRequest[]> {
|
|
return api.get<LidarrRequest[]>('/api/requests');
|
|
}
|
|
|
|
export async function getRequest(id: string): Promise<LidarrRequest> {
|
|
return api.get<LidarrRequest>(`/api/requests/${id}`);
|
|
}
|
|
|
|
// Server returns the cancelled row body (not 204) so callers can patch the
|
|
// cache without a refetch. api.del's return type is fixed to null, so we
|
|
// drop down to apiFetch here to keep typing honest.
|
|
export async function cancelRequest(id: string): Promise<LidarrRequest> {
|
|
return apiFetch(`/api/requests/${id}`, { method: 'DELETE' }) as Promise<LidarrRequest>;
|
|
}
|
|
|
|
export function createMyRequestsQuery() {
|
|
return createQuery({
|
|
queryKey: qk.myRequests(),
|
|
queryFn: listMyRequests,
|
|
staleTime: 60_000,
|
|
// Auto-poll while at least one user request is mid-ingest. Stops
|
|
// automatically when all rows are pending/completed/rejected.
|
|
// refetchIntervalInBackground defaults to false → polling pauses
|
|
// while the tab is hidden and resumes on focus.
|
|
refetchInterval: (query) =>
|
|
hasInFlightRequest(query.state.data as LidarrRequest[] | undefined)
|
|
? POLL_INTERVAL_MS
|
|
: false
|
|
});
|
|
}
|