Skip to content

Commit 5329362

Browse files
authored
honor pause for query refresh (#584)
1 parent e52a194 commit 5329362

10 files changed

Lines changed: 141 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Global live update pause: disable automatic query refreshes on browser focus and reconnect, preventing paused workflow detail pages from re-fetching wait data outside the configured refresh interval. [PR #584](https://github.com/riverqueue/riverui/pull/584).
13+
1014
## [v0.16.0] - 2026-05-19
1115

1216
Version 0.16.0 includes support for the all new workflow engine in River Pro v0.24.0, including signals, timers, and greater introspection capabilities.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
focusManager,
3+
QueryClient,
4+
QueryClientProvider,
5+
useQuery,
6+
} from "@tanstack/react-query";
7+
import { act, render, waitFor } from "@testing-library/react";
8+
import { afterEach, describe, expect, it, vi } from "vitest";
9+
10+
import { refreshQueryOptions } from "./RefreshSettings.query";
11+
12+
describe("refreshQueryOptions", () => {
13+
afterEach(() => {
14+
focusManager.setFocused(undefined);
15+
vi.restoreAllMocks();
16+
});
17+
18+
it("prevents automatic focus refetches when live updates are paused", async () => {
19+
const queryFn = vi.fn<() => Promise<string>>().mockResolvedValue("loaded");
20+
const queryClient = renderQuery({ intervalMs: 0, queryFn });
21+
22+
await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(1));
23+
24+
await act(async () => {
25+
focusManager.setFocused(false);
26+
focusManager.setFocused(true);
27+
});
28+
29+
expect(queryFn).toHaveBeenCalledTimes(1);
30+
queryClient.clear();
31+
});
32+
33+
it("allows automatic focus refetches when live updates are enabled", async () => {
34+
const queryFn = vi.fn<() => Promise<string>>().mockResolvedValue("loaded");
35+
const queryClient = renderQuery({ intervalMs: 2000, queryFn });
36+
37+
await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(1));
38+
39+
await act(async () => {
40+
focusManager.setFocused(false);
41+
focusManager.setFocused(true);
42+
});
43+
44+
await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2));
45+
queryClient.clear();
46+
});
47+
});
48+
49+
const renderQuery = ({
50+
intervalMs,
51+
queryFn,
52+
}: {
53+
intervalMs: number;
54+
queryFn: () => Promise<string>;
55+
}): QueryClient => {
56+
const queryClient = new QueryClient({
57+
defaultOptions: {
58+
queries: {
59+
retry: false,
60+
},
61+
},
62+
});
63+
64+
const Probe = () => {
65+
useQuery({
66+
queryFn,
67+
queryKey: ["refresh-settings-test"],
68+
...refreshQueryOptions(intervalMs),
69+
});
70+
return null;
71+
};
72+
73+
render(
74+
<QueryClientProvider client={queryClient}>
75+
<Probe />
76+
</QueryClientProvider>,
77+
);
78+
79+
return queryClient;
80+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export type RefreshQueryOptions = {
2+
refetchInterval: false | number;
3+
refetchOnReconnect: boolean;
4+
refetchOnWindowFocus: boolean;
5+
};
6+
7+
export const refreshQueryOptions = (
8+
intervalMs: number,
9+
): RefreshQueryOptions => {
10+
const enabled = intervalMs > 0;
11+
12+
return {
13+
refetchInterval: enabled ? intervalMs : false,
14+
refetchOnReconnect: enabled,
15+
refetchOnWindowFocus: enabled,
16+
};
17+
};

src/routes/jobs/$jobId.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import JobDetail from "@components/JobDetail";
22
import JobNotFound from "@components/JobNotFound";
33
import { useRefreshSetting } from "@contexts/RefreshSettings.hook";
4+
import { refreshQueryOptions } from "@contexts/RefreshSettings.query";
45
import {
56
cancelJobs,
67
deleteJobs,
@@ -30,7 +31,6 @@ export const Route = createFileRoute("/jobs/$jobId")({
3031
queryOptions: {
3132
queryKey: getJobKey(jobId),
3233
queryFn: getJob,
33-
refetchInterval: 2000,
3434
signal: abortController.signal,
3535
},
3636
};
@@ -58,7 +58,10 @@ function JobComponent() {
5858
const { queryOptions } = Route.useRouteContext();
5959
const refreshSettings = useRefreshSetting();
6060
const queryOptionsWithRefresh = useMemo(
61-
() => ({ ...queryOptions, refetchInterval: refreshSettings.intervalMs }),
61+
() => ({
62+
...queryOptions,
63+
...refreshQueryOptions(refreshSettings.intervalMs),
64+
}),
6265
[queryOptions, refreshSettings.intervalMs],
6366
);
6467

src/routes/jobs/index.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { Filter, FilterTypeId } from "@components/job-search/JobSearch";
22
import JobList from "@components/JobList";
33
import { useRefreshSetting } from "@contexts/RefreshSettings.hook";
4+
import {
5+
type RefreshQueryOptions,
6+
refreshQueryOptions,
7+
} from "@contexts/RefreshSettings.query";
48
import { defaultValues, jobSearchSchema } from "@routes/jobs/index.schema";
59
import {
610
cancelJobs,
@@ -82,7 +86,7 @@ function JobsIndexComponent() {
8286
const navigate = Route.useNavigate();
8387
const { id, limit, state, kind, queue, priority } = Route.useLoaderDeps();
8488
const refreshSettings = useRefreshSetting();
85-
const refetchInterval = refreshSettings.intervalMs;
89+
const refreshOptions = refreshQueryOptions(refreshSettings.intervalMs);
8690
const [pauseRefetches, setJobRefetchesPaused] = useState(false);
8791
const queryClient = useQueryClient();
8892

@@ -98,11 +102,11 @@ function JobsIndexComponent() {
98102
},
99103
{
100104
pauseRefetches,
101-
refetchInterval,
105+
refreshOptions,
102106
},
103107
),
104108
);
105-
const statesQuery = useQuery(statesQueryOptions({ refetchInterval }));
109+
const statesQuery = useQuery(statesQueryOptions(refreshOptions));
106110

107111
const canShowFewer = limit > minimumLimit;
108112
const canShowMore = limit < maximumLimit;
@@ -339,7 +343,7 @@ const jobsQueryOptions = (
339343
queue?: string[];
340344
state: JobState;
341345
},
342-
opts?: { pauseRefetches: boolean; refetchInterval: number },
346+
opts?: { pauseRefetches: boolean; refreshOptions: RefreshQueryOptions },
343347
) => {
344348
const keepPreviousDataUnlessStateChanged: PlaceholderDataFunction<
345349
JobMinimal[],
@@ -363,13 +367,17 @@ const jobsQueryOptions = (
363367
}),
364368
queryFn: listJobs,
365369
placeholderData: keepPreviousDataUnlessStateChanged,
366-
refetchInterval: !opts?.pauseRefetches && opts?.refetchInterval,
370+
...(opts
371+
? opts.pauseRefetches
372+
? refreshQueryOptions(0)
373+
: opts.refreshOptions
374+
: {}),
367375
});
368376
};
369377

370-
const statesQueryOptions = (opts?: { refetchInterval: number }) =>
378+
const statesQueryOptions = (opts?: RefreshQueryOptions) =>
371379
queryOptions({
372380
queryKey: countsByStateKey(),
373381
queryFn: countsByState,
374-
refetchInterval: opts?.refetchInterval,
382+
...opts,
375383
});

src/routes/periodic-jobs/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import PeriodicJobList from "@components/PeriodicJobList";
22
import PeriodicJobListEmptyState from "@components/PeriodicJobListEmptyState";
33
import { useRefreshSetting } from "@contexts/RefreshSettings.hook";
4+
import { refreshQueryOptions } from "@contexts/RefreshSettings.query";
45
import { listPeriodicJobs, listPeriodicJobsKey } from "@services/periodicJobs";
56
import { queryOptions, useQuery } from "@tanstack/react-query";
67
import { createFileRoute } from "@tanstack/react-router";
@@ -25,9 +26,9 @@ export const Route = createFileRoute("/periodic-jobs/")({
2526
function PeriodicJobsIndexComponent() {
2627
const { jobsQueryOptions } = Route.useRouteContext();
2728
const refreshSettings = useRefreshSetting();
28-
const refetchInterval = refreshSettings.intervalMs;
29+
const refreshOptions = refreshQueryOptions(refreshSettings.intervalMs);
2930

30-
const query = useQuery({ ...jobsQueryOptions, refetchInterval });
31+
const query = useQuery({ ...jobsQueryOptions, ...refreshOptions });
3132

3233
if (!jobsQueryOptions.enabled) {
3334
return <PeriodicJobListEmptyState hasAny={false} />;

src/routes/queues/$name.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import QueueDetail from "@components/QueueDetail";
22
import { useRefreshSetting } from "@contexts/RefreshSettings.hook";
3+
import { refreshQueryOptions } from "@contexts/RefreshSettings.query";
34
import { listProducers, listProducersKey } from "@services/producers";
45
import {
56
type ConcurrencyConfig,
@@ -64,16 +65,17 @@ function QueueComponent() {
6465
const { name } = Route.useParams();
6566
const { queueQueryOptions, producersQueryOptions } = Route.useRouteContext();
6667
const refreshSettings = useRefreshSetting();
68+
const refreshOptions = refreshQueryOptions(refreshSettings.intervalMs);
6769
const { features } = Route.useRouteContext();
6870
const queryClient = useQueryClient();
6971

7072
const queueQuery = useQuery({
7173
...queueQueryOptions,
72-
refetchInterval: refreshSettings.intervalMs,
74+
...refreshOptions,
7375
});
7476
const producersQuery = useQuery({
7577
...producersQueryOptions,
76-
refetchInterval: refreshSettings.intervalMs,
78+
...refreshOptions,
7779
});
7880

7981
const loading =

src/routes/queues/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import QueueList from "@components/QueueList";
22
import { useRefreshSetting } from "@contexts/RefreshSettings.hook";
3+
import { refreshQueryOptions } from "@contexts/RefreshSettings.query";
34
import {
45
listQueues,
56
listQueuesKey,
@@ -16,7 +17,6 @@ export const Route = createFileRoute("/queues/")({
1617
queryOptions: {
1718
queryKey: listQueuesKey(),
1819
queryFn: listQueues,
19-
refetchInterval: 2000,
2020
signal: abortController.signal,
2121
},
2222
};
@@ -32,7 +32,10 @@ function QueuesIndexComponent() {
3232
const { queryOptions } = Route.useRouteContext();
3333
const refreshSettings = useRefreshSetting();
3434
const queryOptionsWithRefresh = useMemo(
35-
() => ({ ...queryOptions, refetchInterval: refreshSettings.intervalMs }),
35+
() => ({
36+
...queryOptions,
37+
...refreshQueryOptions(refreshSettings.intervalMs),
38+
}),
3639
[queryOptions, refreshSettings.intervalMs],
3740
);
3841

src/routes/workflows/$workflowId.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import WorkflowDetail from "@components/WorkflowDetail";
22
import { useRefreshSetting } from "@contexts/RefreshSettings.hook";
3+
import { refreshQueryOptions } from "@contexts/RefreshSettings.query";
34
import { toastSuccess } from "@services/toast";
45
import {
56
cancelJobs,
@@ -37,7 +38,6 @@ export const Route = createFileRoute("/workflows/$workflowId")({
3738
enabled: features.workflowQueries,
3839
queryKey: getWorkflowKey(workflowId),
3940
queryFn: getWorkflow,
40-
refetchInterval: 1000,
4141
signal: abortController.signal,
4242
},
4343
};
@@ -66,7 +66,10 @@ function WorkflowComponent() {
6666
const refreshSettings = useRefreshSetting();
6767
const queryClient = useQueryClient();
6868
const queryOptionsWithRefresh = useMemo(
69-
() => ({ ...queryOptions, refetchInterval: refreshSettings.intervalMs }),
69+
() => ({
70+
...queryOptions,
71+
...refreshQueryOptions(refreshSettings.intervalMs),
72+
}),
7073
[queryOptions, refreshSettings.intervalMs],
7174
);
7275

src/routes/workflows/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import WorkflowList from "@components/WorkflowList";
22
import { useRefreshSetting } from "@contexts/RefreshSettings.hook";
3+
import { refreshQueryOptions } from "@contexts/RefreshSettings.query";
34
import { WorkflowState } from "@services/types";
45
import { listWorkflows, listWorkflowsKey } from "@services/workflows";
56
import { queryOptions, useQuery } from "@tanstack/react-query";
@@ -47,11 +48,11 @@ export const Route = createFileRoute("/workflows/")({
4748

4849
function WorkflowsIndexComponent() {
4950
const refreshSettings = useRefreshSetting();
50-
const refetchInterval = refreshSettings.intervalMs;
51+
const refreshOptions = refreshQueryOptions(refreshSettings.intervalMs);
5152
const { features, workflowsQueryOptions } = Route.useRouteContext();
5253
const workflowsQuery = useQuery({
5354
...workflowsQueryOptions,
54-
refetchInterval,
55+
...refreshOptions,
5556
});
5657

5758
return (

0 commit comments

Comments
 (0)