@@ -18,6 +18,13 @@ import {
1818 SafeApiError ,
1919} from "@/types/api-response" ;
2020import { cn } from "@/lib/utils" ;
21+ import {
22+ createComparisonQuery ,
23+ createComparisonRequest ,
24+ isComparisonFetchDuplicate ,
25+ reconcileComparisonData ,
26+ sanitizeSelectedLanguages ,
27+ } from "@/lib/compare-request" ;
2128
2229type ComparisonData = {
2330 user1 : UserResult ;
@@ -45,26 +52,6 @@ type UsernameErrors = {
4552
4653const 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-
6855function 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