Skip to content

Commit dab4a96

Browse files
authored
fix: prevent comparison swap refetches (#200)
Co-authored-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com>
1 parent 9b7a578 commit dab4a96

3 files changed

Lines changed: 330 additions & 72 deletions

File tree

components/home-page-client.tsx

Lines changed: 91 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ import {
1818
SafeApiError,
1919
} from "@/types/api-response";
2020
import { cn } from "@/lib/utils";
21+
import {
22+
createComparisonQuery,
23+
createComparisonRequest,
24+
isComparisonFetchDuplicate,
25+
reconcileComparisonData,
26+
sanitizeSelectedLanguages,
27+
} from "@/lib/compare-request";
2128

2229
type ComparisonData = {
2330
user1: UserResult;
@@ -45,26 +52,6 @@ type UsernameErrors = {
4552

4653
const EXIT_ANIMATION_MS = 240;
4754

48-
function sanitizeSelectedLanguages(languages: string[]): string[] {
49-
const seen = new Set<string>();
50-
const output: string[] = [];
51-
52-
for (const language of languages) {
53-
const trimmed = language.trim();
54-
const normalized = trimmed.toLowerCase();
55-
if (!trimmed || seen.has(normalized)) {
56-
continue;
57-
}
58-
output.push(trimmed);
59-
seen.add(normalized);
60-
if (output.length >= 5) {
61-
break;
62-
}
63-
}
64-
65-
return output;
66-
}
67-
6855
function normalizeUsers(body: ApiResponse): { user1: UserResult; user2: UserResult } | null {
6956
if (body.users && body.users.length >= 2) {
7057
return { user1: body.users[0], user2: body.users[1] };
@@ -100,6 +87,9 @@ export function HomePageClient() {
10087
const lastFetchedKeyRef = useRef<string | null>(null);
10188
const inFlightFetchKeyRef = useRef<string | null>(null);
10289
const inFlightPromiseRef = useRef<Promise<void> | null>(null);
90+
const latestRequestRef = useRef(
91+
createComparisonRequest(initialUsername1, initialUsername2, initialSelectedLanguages),
92+
);
10393
const hideTimerRef = useRef<number | null>(null);
10494

10595
const localizeErrorMessage = (message?: string, details?: SafeApiError) => {
@@ -206,76 +196,72 @@ export function HomePageClient() {
206196
setGeneralError(localizedMessage);
207197
};
208198

209-
const createFetchKey = (
210-
u1: string,
211-
u2: string,
212-
options: CompareOptions,
213-
) =>
214-
JSON.stringify({
215-
u1,
216-
u2,
217-
selectedLanguages: [...sanitizeSelectedLanguages(options.selectedLanguages)].sort(),
218-
});
219-
220199
const handleCompare = async (
221200
u1: string,
222201
u2: string,
223202
options: CompareOptions,
224203
) => {
225-
const sanitizedLanguages = sanitizeSelectedLanguages(options.selectedLanguages);
226-
const fetchKey = createFetchKey(u1, u2, options);
204+
const request = createComparisonRequest(u1, u2, options.selectedLanguages);
205+
latestRequestRef.current = request;
206+
const fetchKey = request.fetchKey;
227207

228208
if (inFlightFetchKeyRef.current === fetchKey && inFlightPromiseRef.current) {
229209
return inFlightPromiseRef.current;
230210
}
231211

232212
// If we've already fetched this exact comparison and have the data, skip.
233213
if (lastFetchedKeyRef.current === fetchKey && data) {
214+
const reconciled = reconcileComparisonData(data, fetchKey, request);
215+
if (reconciled) {
216+
setData(reconciled);
217+
setDisplayData(reconciled);
218+
}
234219
return Promise.resolve();
235220
}
236221

237222
lastFetchedKeyRef.current = fetchKey;
238223

239224
// update duplicate fetch state for current form values
225+
const currentFetchKey = createComparisonRequest(
226+
username1,
227+
username2,
228+
selectedLanguages,
229+
).fetchKey;
240230
setDisableDuplicateFetch(
241-
Boolean(lastFetchedKeyRef.current === createFetchKey(username1.trim(), username2.trim(), { selectedLanguages }) && (data || inFlightFetchKeyRef.current === fetchKey)),
231+
isComparisonFetchDuplicate(
232+
currentFetchKey,
233+
lastFetchedKeyRef.current,
234+
inFlightFetchKeyRef.current,
235+
Boolean(data),
236+
),
242237
);
243238

244239
const requestPromise = (async () => {
245240
if (options.updateUrl !== false) {
246-
const params = new URLSearchParams();
247-
params.append("username", u1);
248-
params.append("username", u2);
249-
for (const language of sanitizedLanguages) {
250-
params.append("selectedLanguage", language);
251-
}
252-
router.push(`/?${params.toString()}`, { scroll: false });
241+
router.push(`/?${createComparisonQuery(request)}`, { scroll: false });
253242
}
254243

255244
setLoading(true);
256245
resetErrors();
257246

258247
try {
259-
const requestParams = new URLSearchParams();
260-
requestParams.append("username", u1);
261-
requestParams.append("username", u2);
262-
for (const language of sanitizedLanguages) {
263-
requestParams.append("selectedLanguage", language);
264-
}
265-
266-
const res = await fetch(`/api/compare?${requestParams.toString()}`);
248+
const res = await fetch(`/api/compare?${createComparisonQuery(request)}`);
267249

268250
const body: ApiResponse = await res.json();
269251
if (!res.ok) {
252+
if (latestRequestRef.current.fetchKey !== fetchKey) {
253+
return;
254+
}
270255
setData(null);
271-
applyApiError(u1, u2, body);
256+
applyApiError(latestRequestRef.current.user1, latestRequestRef.current.user2, body);
272257
return;
273258
}
274259
const users = normalizeUsers(body);
275260

276261
if (!body.success || !users) {
262+
if (latestRequestRef.current.fetchKey !== fetchKey) return;
277263
setData(null);
278-
applyApiError(u1, u2, body);
264+
applyApiError(latestRequestRef.current.user1, latestRequestRef.current.user2, body);
279265
return;
280266
}
281267

@@ -296,9 +282,25 @@ export function HomePageClient() {
296282
scoreVersion: body.scoreVersion,
297283
};
298284

299-
setData(nextData);
300-
setDisplayData(nextData);
285+
const reconciled = reconcileComparisonData(
286+
nextData,
287+
fetchKey,
288+
latestRequestRef.current,
289+
);
290+
if (!reconciled) {
291+
if (latestRequestRef.current.fetchKey === fetchKey) {
292+
setData(null);
293+
setGeneralError(t("error.generic"));
294+
}
295+
return;
296+
}
297+
298+
setData(reconciled);
299+
setDisplayData(reconciled);
301300
} catch (err: unknown) {
301+
if (latestRequestRef.current.fetchKey !== fetchKey) {
302+
return;
303+
}
302304
setData(null);
303305
setUsernameErrors({
304306
username1: null,
@@ -309,8 +311,8 @@ export function HomePageClient() {
309311
if (inFlightFetchKeyRef.current === fetchKey) {
310312
inFlightFetchKeyRef.current = null;
311313
inFlightPromiseRef.current = null;
314+
setLoading(false);
312315
}
313-
setLoading(false);
314316
}
315317
})();
316318

@@ -319,7 +321,12 @@ export function HomePageClient() {
319321

320322
// mark duplicate fetch disabled while request is in-flight
321323
setDisableDuplicateFetch(
322-
Boolean(lastFetchedKeyRef.current === createFetchKey(username1.trim(), username2.trim(), { selectedLanguages }) && (data || inFlightFetchKeyRef.current === fetchKey)),
324+
isComparisonFetchDuplicate(
325+
currentFetchKey,
326+
lastFetchedKeyRef.current,
327+
inFlightFetchKeyRef.current,
328+
Boolean(data),
329+
),
323330
);
324331

325332
return requestPromise;
@@ -332,21 +339,14 @@ export function HomePageClient() {
332339
setSelectedLanguages(languages);
333340

334341
if (!u1 || !u2) {
342+
latestRequestRef.current = createComparisonRequest(u1, u2, languages);
335343
lastFetchedKeyRef.current = null;
336344
setData(null);
337345
resetErrors();
338346
setDisableDuplicateFetch(false);
339347
return;
340348
}
341349

342-
const nextKey = createFetchKey(u1, u2, {
343-
selectedLanguages: languages,
344-
});
345-
346-
if (lastFetchedKeyRef.current === nextKey && data) {
347-
return;
348-
}
349-
350350
void handleCompare(u1, u2, {
351351
selectedLanguages: languages,
352352
updateUrl: false,
@@ -399,14 +399,21 @@ export function HomePageClient() {
399399

400400

401401
useEffect(() => {
402-
const currentFetchKey = createFetchKey(username1.trim(), username2.trim(), {
402+
const currentFetchKey = createComparisonRequest(
403+
username1,
404+
username2,
403405
selectedLanguages,
404-
});
406+
).fetchKey;
405407

406408
const lastKey = lastFetchedKeyRef.current;
407409
const inFlightKey = inFlightFetchKeyRef.current;
408410

409-
const disabled = Boolean(lastKey === currentFetchKey && (data || inFlightKey === currentFetchKey));
411+
const disabled = isComparisonFetchDuplicate(
412+
currentFetchKey,
413+
lastKey,
414+
inFlightKey,
415+
Boolean(data),
416+
);
410417
setDisableDuplicateFetch(disabled);
411418
}, [username1, username2, selectedLanguages, data, loading]);
412419

@@ -430,6 +437,7 @@ export function HomePageClient() {
430437
resetErrors();
431438
inFlightFetchKeyRef.current = null;
432439
inFlightPromiseRef.current = null;
440+
latestRequestRef.current = createComparisonRequest("", "", []);
433441
setDisableDuplicateFetch(false);
434442
setUsername1("");
435443
setUsername2("");
@@ -440,16 +448,27 @@ export function HomePageClient() {
440448
const swapUsers = () => {
441449
const nextUsername1 = username2;
442450
const nextUsername2 = username1;
451+
const nextRequest = createComparisonRequest(
452+
nextUsername1,
453+
nextUsername2,
454+
selectedLanguages,
455+
);
456+
latestRequestRef.current = nextRequest;
443457

444458
setUsername1(nextUsername1);
445459
setUsername2(nextUsername2);
446-
router.push(
447-
`/?username=${encodeURIComponent(nextUsername1)}&username=${encodeURIComponent(nextUsername2)}`,
448-
{ scroll: false },
449-
);
460+
router.push(`/?${createComparisonQuery(nextRequest)}`, { scroll: false });
450461

451-
if (!data) return;
452-
setData((current) => (current ? { ...current, user1: current.user2, user2: current.user1 } : current));
462+
setData((current) =>
463+
current
464+
? reconcileComparisonData(current, nextRequest.fetchKey, nextRequest)
465+
: current,
466+
);
467+
setDisplayData((current) =>
468+
current
469+
? reconcileComparisonData(current, nextRequest.fetchKey, nextRequest)
470+
: current,
471+
);
453472
};
454473

455474
return (

0 commit comments

Comments
 (0)