Skip to content

Commit 282f3f0

Browse files
committed
perf(app): streamline server-function requests
Move growing lookup payloads to POST, batch good metadata reads, trim picker catalogs from block solve responses, and give app-shell polling a single cache-backed owner.
1 parent 821d2fd commit 282f3f0

21 files changed

Lines changed: 273 additions & 155 deletions

app/src/components/app-live-queries.test.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
// @vitest-environment jsdom
2-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2+
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
33
import { act, cleanup, render } from "@testing-library/react";
44
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
55

66
import { AppLiveQueries } from "./app-live-queries.tsx";
7+
import {
8+
bridgeStatusSubscription,
9+
logisticsContextSubscription,
10+
researchHorizonSubscription,
11+
undoStatusSubscription,
12+
} from "../lib/live-query-options.ts";
713

814
const fns = vi.hoisted(() => ({
915
bridge: vi.fn(),
@@ -30,6 +36,14 @@ afterEach(() => {
3036
});
3137

3238
describe("AppLiveQueries", () => {
39+
function StatusSubscribers() {
40+
useQuery(bridgeStatusSubscription);
41+
useQuery(researchHorizonSubscription);
42+
useQuery(logisticsContextSubscription);
43+
useQuery(undoStatusSubscription);
44+
return null;
45+
}
46+
3347
it("owns one interval at each live-status cadence", async () => {
3448
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
3549
render(
@@ -53,4 +67,30 @@ describe("AppLiveQueries", () => {
5367
expect(fns.logistics).toHaveBeenCalledTimes(2);
5468
expect(fns.undo).toHaveBeenCalledTimes(2);
5569
});
70+
71+
it("lets later responsive-layout subscribers reuse the fresh poll result", async () => {
72+
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
73+
render(
74+
<QueryClientProvider client={client}>
75+
<AppLiveQueries />
76+
</QueryClientProvider>,
77+
);
78+
await act(async () => {
79+
await Promise.resolve();
80+
});
81+
82+
render(
83+
<QueryClientProvider client={client}>
84+
<StatusSubscribers />
85+
</QueryClientProvider>,
86+
);
87+
await act(async () => {
88+
await Promise.resolve();
89+
});
90+
91+
expect(fns.bridge).toHaveBeenCalledTimes(1);
92+
expect(fns.horizon).toHaveBeenCalledTimes(1);
93+
expect(fns.logistics).toHaveBeenCalledTimes(1);
94+
expect(fns.undo).toHaveBeenCalledTimes(1);
95+
});
5696
});

app/src/components/app-live-queries.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { useQuery } from "@tanstack/react-query";
22

3-
import { bridgeStatusFn } from "../server/bridge/fns";
4-
import { logisticsContextFn, researchHorizonFn } from "../server/factorio";
5-
import { undoStatusFn } from "../server/undo";
3+
import {
4+
bridgeStatusQuery,
5+
LIVE_QUERY_INTERVALS,
6+
logisticsContextQuery,
7+
researchHorizonQuery,
8+
undoStatusQuery,
9+
} from "../lib/live-query-options";
610

711
/**
812
* Single owner for the app-shell's recurring status reads. Desktop and mobile
@@ -13,24 +17,20 @@ import { undoStatusFn } from "../server/undo";
1317
*/
1418
export function AppLiveQueries() {
1519
useQuery({
16-
queryKey: ["bridgeStatus"],
17-
queryFn: () => bridgeStatusFn(),
18-
refetchInterval: 2000,
20+
...bridgeStatusQuery,
21+
refetchInterval: LIVE_QUERY_INTERVALS.bridge,
1922
});
2023
useQuery({
21-
queryKey: ["researchHorizon"],
22-
queryFn: () => researchHorizonFn(),
23-
refetchInterval: 4000,
24+
...researchHorizonQuery,
25+
refetchInterval: LIVE_QUERY_INTERVALS.horizon,
2426
});
2527
useQuery({
26-
queryKey: ["logisticsContext"],
27-
queryFn: () => logisticsContextFn(),
28-
refetchInterval: 5000,
28+
...logisticsContextQuery,
29+
refetchInterval: LIVE_QUERY_INTERVALS.logistics,
2930
});
3031
useQuery({
31-
queryKey: ["undoStatus"],
32-
queryFn: () => undoStatusFn(),
33-
refetchInterval: 5000,
32+
...undoStatusQuery,
33+
refetchInterval: LIVE_QUERY_INTERVALS.undo,
3434
});
3535

3636
return null;

app/src/components/block/fuel-picker-dialog.tsx

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,43 @@
1-
import { useQueryClient } from "@tanstack/react-query";
1+
import { useQuery, useQueryClient } from "@tanstack/react-query";
22
import { Star } from "lucide-react";
3-
import { setFavoriteFuelFn } from "../../server/factorio";
3+
import { fuelPickerOptionsFn, setFavoriteFuelFn } from "../../server/factorio";
44
import { Badge } from "#/components/ui/badge.tsx";
55
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "#/components/ui/dialog.tsx";
66
import { Icon } from "../../lib/icons";
77
import { fmtJ } from "./format.ts";
88
import { rowBtn } from "./styles.ts";
9+
import { Skeleton } from "#/components/ui/skeleton.tsx";
910

1011
/** Fuel picker — choose what a SOLID burner burns (energy value shown to
1112
* compare), with the favorite star (#18). Favorites are app-level prefs;
1213
* toggling one refetches the solve so its ☆ updates without touching the
1314
* block's picks. Fluid burners never open this: unfiltered ones draw from the
1415
* shared fluid-fuel pool and filtered ones are pinned to one fluid (#25). */
1516
export function FuelPickerDialog({
17+
recipe,
1618
recipeDisplay,
17-
fuels,
19+
machine,
1820
current,
1921
onPick,
2022
onClose,
2123
}: {
24+
recipe: string;
2225
recipeDisplay: string;
23-
fuels: {
24-
name: string;
25-
display: string | null;
26-
kind: string;
27-
fuelValueJ: number | null;
28-
favorite: boolean;
29-
}[];
26+
machine: string;
3027
/** the fuel the current solve burns for this recipe */
3128
current: string | null;
3229
onPick: (fuel: string) => void;
3330
onClose: () => void;
3431
}) {
3532
const qc = useQueryClient();
33+
const fuels = useQuery({
34+
queryKey: ["fuelOptions", recipe, machine],
35+
queryFn: () => fuelPickerOptionsFn({ data: { recipe, machine } }),
36+
staleTime: 60_000,
37+
});
3638
const toggleFavorite = (fuel: string, isFav: boolean) => {
3739
void setFavoriteFuelFn({ data: { fuel, clear: isFav } }).then(() => {
40+
void qc.invalidateQueries({ queryKey: ["fuelOptions"] });
3841
void qc.invalidateQueries({ queryKey: ["solve"] });
3942
});
4043
};
@@ -50,7 +53,14 @@ export function FuelPickerDialog({
5053
<DialogTitle className="truncate">Fuel for {recipeDisplay}</DialogTitle>
5154
</DialogHeader>
5255
<div className="min-h-0 flex-1 overflow-auto p-2">
53-
{fuels.map((f) => {
56+
{fuels.isLoading && (
57+
<div className="space-y-1.5 p-2">
58+
<Skeleton className="h-9 w-full" />
59+
<Skeleton className="h-9 w-full" />
60+
<Skeleton className="h-9 w-3/4" />
61+
</div>
62+
)}
63+
{fuels.data?.map((f) => {
5464
const cur = current === f.name;
5565
return (
5666
<button
@@ -82,7 +92,7 @@ export function FuelPickerDialog({
8292
</button>
8393
);
8494
})}
85-
{!fuels.length && (
95+
{fuels.data?.length === 0 && (
8696
<div className="px-2 py-1 text-sm text-muted-foreground">
8797
no fuels for this machine's categories
8898
</div>

app/src/components/block/solve-view.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
import type { computeBlock } from "../../server/block-compute.server.ts";
55
import type { ResolvedLogistics } from "../../lib/logistics";
66

7-
type CoreSolveResult = Awaited<ReturnType<typeof computeBlock>>;
7+
type ComputedBlockResult = Awaited<ReturnType<typeof computeBlock>>;
8+
type CoreSolveResult = Omit<ComputedBlockResult, "recipes">;
89

910
/** The editor enriches authoritative core rows with lazy presentation-only
1011
* module hints. They are deliberately absent from `computeBlock` itself. */

app/src/components/block/sushi-planner.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { planSushi, type ResolvedLogistics, type SushiFlow } from "../../lib/log
55
import { constantCombinatorBlueprint, encodeBlueprint } from "../../lib/blueprint";
66
import { fmtSpoilTime, Icon, useSpoilables } from "../../lib/icons";
77
import { toast } from "../../lib/toast-store";
8-
import { bridgeBlueprintFn, bridgeStatusFn, sushiTraceInfoFn } from "../../server/bridge/fns";
8+
import { bridgeStatusSubscription } from "../../lib/live-query-options";
9+
import { bridgeBlueprintFn, sushiTraceInfoFn } from "../../server/bridge/fns";
910
import { fmtCount } from "./format.ts";
1011
import { Button } from "#/components/ui/button.tsx";
1112
import { Callout } from "#/components/ui/callout.tsx";
@@ -127,12 +128,7 @@ export function SushiPlanner({
127128
// only the block-inputs section ships ACTIVE (imports are what's injected from
128129
// outside, so they're what needs gating); outputs and intermediates free-flow,
129130
// their set-points parked in disabled sections as flippable reference.
130-
const bridge = useQuery({
131-
queryKey: ["bridgeStatus"],
132-
queryFn: () => bridgeStatusFn(),
133-
enabled: open,
134-
refetchInterval: 3000,
135-
});
131+
const bridge = useQuery(bridgeStatusSubscription);
136132
const peer = bridge.data?.lastPeer ?? null;
137133
const connected = peer != null && Date.now() - peer.lastSeenMs < FRESH_MS;
138134
// the mod's ALT+B loop tracer pushes its measurement here — offer, don't overwrite

app/src/components/bridge-card.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useState } from "react";
22
import { useMutation, useQuery } from "@tanstack/react-query";
33
import { AlertTriangle, RefreshCw } from "lucide-react";
4-
import { bridgeRequestSyncFn, bridgeStatusFn } from "../server/bridge/fns";
4+
import { bridgeRequestSyncFn } from "../server/bridge/fns";
5+
import { bridgeStatusSubscription } from "../lib/live-query-options";
56
import { Button } from "#/components/ui/button.tsx";
67
import { Callout } from "#/components/ui/callout.tsx";
78
import { Card, CardHeader, CardTitle } from "#/components/ui/card.tsx";
@@ -14,10 +15,7 @@ const FRESH_MS = 6000;
1415

1516
/** Live UDP bridge status from the app-shell query owner. */
1617
export function BridgeCard() {
17-
const status = useQuery({
18-
queryKey: ["bridgeStatus"],
19-
queryFn: () => bridgeStatusFn(),
20-
});
18+
const status = useQuery(bridgeStatusSubscription);
2119
const s = status.data;
2220
const peer = s?.lastPeer ?? null;
2321
const connected = peer != null && Date.now() - peer.lastSeenMs < FRESH_MS;

app/src/components/bridge-indicator.test.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ vi.mock("../server/bridge/fns", () => ({ bridgeStatusFn: bridgeStatus }));
2222
afterEach(cleanup);
2323
beforeEach(() => bridgeStatus.mockReset());
2424

25-
function renderIndicator() {
25+
async function renderIndicator() {
2626
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
27+
client.setQueryData(["bridgeStatus"], await bridgeStatus());
2728
return render(
2829
<QueryClientProvider client={client}>
2930
<BridgeIndicator />
@@ -42,7 +43,7 @@ describe("BridgeIndicator", () => {
4243
appProtocolVersion: 4,
4344
lastPeer: { lastSeenMs: Date.now(), protocolVersion: 4, player: "jim" },
4445
});
45-
const { findByText, findByRole, getByTestId } = renderIndicator();
46+
const { findByText, findByRole, getByTestId } = await renderIndicator();
4647
expect(await findByText("game linked")).toBeTruthy();
4748
expect(dot(getByTestId("bridge-link")).className).toContain("bg-success");
4849
fireEvent.focus(getByTestId("bridge-link"));
@@ -57,7 +58,7 @@ describe("BridgeIndicator", () => {
5758
appProtocolVersion: 4,
5859
lastPeer: { lastSeenMs: Date.now(), protocolVersion: 3, player: "jim" },
5960
});
60-
const { findByText, getByTestId } = renderIndicator();
61+
const { findByText, getByTestId } = await renderIndicator();
6162
expect(await findByText("mod mismatch")).toBeTruthy();
6263
expect(dot(getByTestId("bridge-link")).className).toContain("bg-destructive");
6364
});
@@ -70,7 +71,7 @@ describe("BridgeIndicator", () => {
7071
appProtocolVersion: 4,
7172
lastPeer: null,
7273
});
73-
const { findByText, getByTestId } = renderIndicator();
74+
const { findByText, getByTestId } = await renderIndicator();
7475
expect(await findByText("no game")).toBeTruthy();
7576
expect(dot(getByTestId("bridge-link")).className).toContain("bg-warning");
7677
});
@@ -83,7 +84,7 @@ describe("BridgeIndicator", () => {
8384
appProtocolVersion: 4,
8485
lastPeer: { lastSeenMs: Date.now() - 60_000, protocolVersion: 4, player: "jim" },
8586
});
86-
const { findByText } = renderIndicator();
87+
const { findByText } = await renderIndicator();
8788
expect(await findByText("no game")).toBeTruthy();
8889
});
8990

@@ -96,7 +97,7 @@ describe("BridgeIndicator", () => {
9697
appProtocolVersion: 4,
9798
lastPeer: null,
9899
});
99-
const { findByText, findByRole, getByTestId } = renderIndicator();
100+
const { findByText, findByRole, getByTestId } = await renderIndicator();
100101
expect(await findByText("bridge error")).toBeTruthy();
101102
fireEvent.focus(getByTestId("bridge-link"));
102103
expect((await findByRole("tooltip")).textContent).toContain("EADDRINUSE");

app/src/components/bridge-indicator.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Link } from "@tanstack/react-router";
22
import { useQuery } from "@tanstack/react-query";
3-
import { bridgeStatusFn } from "../server/bridge/fns";
3+
import { bridgeStatusSubscription } from "../lib/live-query-options";
44
import { Tooltip } from "#/components/ui/tooltip.tsx";
55

66
/** Treat the mod as connected if we've heard from it within this window (its
@@ -12,10 +12,7 @@ const FRESH_MS = 6000;
1212
* ["bridgeStatus"] query owned by AppLiveQueries, which also starts the UDP
1313
* listener and keeps the bridge live on every page. */
1414
export function BridgeIndicator() {
15-
const status = useQuery({
16-
queryKey: ["bridgeStatus"],
17-
queryFn: () => bridgeStatusFn(),
18-
});
15+
const status = useQuery(bridgeStatusSubscription);
1916
const s = status.data;
2017
const peer = s?.lastPeer ?? null;
2118
const connected = peer != null && Date.now() - peer.lastSeenMs < FRESH_MS;

app/src/components/drift-modal.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AlertTriangle, Check, Loader2, RefreshCw, X, type LucideIcon } from "lu
33
import { Link } from "@tanstack/react-router";
44
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
55
import { factorioRunningFn, modDriftFn, startDataSyncFn, syncStateFn } from "../server/factorio";
6-
import { bridgeStatusFn } from "../server/bridge/fns";
6+
import { bridgeStatusSubscription } from "../lib/live-query-options";
77
import { driftModal, useDriftModalOpen } from "../lib/drift-store";
88
import { type StepStatus, stepStatuses, stepsForRun } from "../lib/sync-steps";
99
import { DriftChanges } from "./drift-changes";
@@ -70,10 +70,7 @@ export function DriftModal() {
7070
queryFn: () => syncStateFn(),
7171
refetchInterval: (q) => (RUNNING.has(q.state.data?.phase ?? "") ? 1000 : false),
7272
});
73-
const bridge = useQuery({
74-
queryKey: ["bridgeStatus"],
75-
queryFn: () => bridgeStatusFn(),
76-
});
73+
const bridge = useQuery(bridgeStatusSubscription);
7774
// Proactively check whether the game is running while the prompt is open, so we
7875
// can warn and block the sync (it would just fail on the engine's instance lock).
7976
const gameRunning = useQuery({

app/src/components/horizon-menu.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useQuery } from "@tanstack/react-query";
22
import { useState } from "react";
33
import { Clock } from "lucide-react";
4-
import { researchHorizonFn } from "../server/factorio";
4+
import { researchHorizonSubscription } from "../lib/live-query-options";
55
import { HorizonHelpButton } from "./horizon-help";
66
import { HorizonPicker, horizonLabel } from "./horizon-picker";
77
import { Button } from "#/components/ui/button.tsx";
@@ -18,10 +18,7 @@ import {
1818
* opens a dialog to change it (Now / Future / Up to target). */
1919
export function HorizonMenu() {
2020
const [open, setOpen] = useState(false);
21-
const h = useQuery({
22-
queryKey: ["researchHorizon"],
23-
queryFn: () => researchHorizonFn(),
24-
});
21+
const h = useQuery(researchHorizonSubscription);
2522
const label = h.data ? horizonLabel(h.data) : "…";
2623

2724
return (

0 commit comments

Comments
 (0)