Skip to content

Conversation

@tahminator
Copy link
Owner

@tahminator tahminator commented Jan 14, 2026

635

Description of changes

Checklist before review

  • I have done a thorough self-review of the PR
  • Copilot has reviewed my latest changes, and all comments have been fixed and/or closed.
  • If I have made database changes, I have made sure I followed all the db repo rules listed in the wiki here. (check if no db changes)
  • All tests have passed
  • I have successfully deployed this PR to staging
  • I have done manual QA in both dev (and staging if possible) and attached screenshots below.

Screenshots

Dev

Staging

@github-actions
Copy link
Contributor

Title

635: wip


PR Type

Enhancement, Bug fix


Description

  • Add type-safe React Query key generation

  • Refactor queries to use ApiURL.queryKey

  • Improve leaderboard query invalidations

  • Fix lobby navigation joinCode check


Diagram Walkthrough

flowchart LR
  A["ApiURL enhancements"] -- "generate query keys" --> B["queryKey/_generateKey/prefix"]
  B -- "used by" --> C["Leaderboard queries refactor"]
  B -- "used by" --> D["Submission query refactor"]
  C -- "invalidate via prefix" --> E["React Query cache"]
  F["Lobby navigation"] -- "check joinCode before redirect" --> G["Stable routing"]
Loading

File Walkthrough

Relevant files
Bug fix
useDuelNavigation.ts
Guard lobby navigation by joinCode presence                           

js/src/app/duel/_hooks/useDuelNavigation.ts

  • Guard navigate with lobby.joinCode
  • Update effect deps to include joinCode
+2/-2     
Enhancement
apiURL.ts
Add type-safe React Query key generation to ApiURL             

js/src/lib/api/common/apiURL.ts

  • Add type utilities for path segments
  • Implement _generateKey, queryKey, prefix
  • Store instance _key and expose queryKey getter
  • Enhance typing for query key segments
+146/-0 
index.ts
Refactor leaderboard queries to ApiURL queryKey                   

js/src/lib/api/queries/leaderboard/index.ts

  • Refactor to use ApiURL.create with queryKey
  • Replace manual query keys and fetchers
  • Use ApiURL.prefix for invalidations
  • Inline previous helper functions and remove them
+172/-234
index.ts
Use ApiURL queryKey for submission details query                 

js/src/lib/api/queries/submissions/index.ts

  • Use ApiURL.create to build URL and queryKey
  • Inline fetch logic within useQuery
  • Remove separate fetch helper
+13/-18 

@github-actions
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Redirect Loop

The effect now depends on 'data?.payload?.lobby.joinCode' and navigates to '/duel/current' when present, otherwise '/duel'. If either route triggers the same data refetch updating joinCode quickly, this could cause oscillating redirects. Verify navigation stability and guard against repeated navigations.

useEffect(() => {
  if (data?.success && data.payload.lobby.joinCode) {
    navigate("/duel/current");
  } else {
    navigate("/duel");
  }
}, [data?.payload?.lobby.joinCode, data?.success, navigate]);
Query Key Stability

The generated React Query keys include the entire queries and params objects. If object key ordering is not guaranteed, keys might be unstable across renders. Ensure a consistent serialization/order of query keys or sorted entries to avoid unnecessary cache misses.

private static _generateKey<
  const TPath extends PathsKey,
  const TMethod extends HttpMethodUpper,
  const TParams,
  const TQueries,
>(
  path: TPath,
  options: {
    method: TMethod;
    params: TParams;
    queries: TQueries;
  },
): readonly [
  ...PathSegments<TPath>,
  { method: TMethod; params: TParams; queries: TQueries },
] {
  const segments = path.split("/").filter((segment) => segment.length > 0);
  return [
    ...segments,
    {
      method: options.method,
      params: options.params,
      queries: options.queries,
    },
  ] as unknown as readonly [
    ...PathSegments<TPath>,
    { method: TMethod; params: TParams; queries: TQueries },
  ];
}
Invalidation Prefix Consistency

Invalidate calls now use ApiURL.prefix with segments from paths. Confirm that all queries for these resources are created with the same segmented prefix so invalidations hit the intended caches (e.g., 'leaderboard' vs '/api/leaderboard' mismatches).

onSuccess: () => {
  queryClient.invalidateQueries({
    queryKey: ApiURL.prefix("/api/leaderboard"),
  });
  queryClient.invalidateQueries({
    queryKey: ApiURL.prefix("/api/leetcode/submission"),
  });
  queryClient.invalidateQueries({
    queryKey: ApiURL.prefix("/api/leetcode/potd"),
  });

Comment on lines +148 to +159
const segments = path.split("/").filter((segment) => segment.length > 0);
return [
...segments,
{
method: options.method,
params: options.params,
queries: options.queries,
},
] as unknown as readonly [
...PathSegments<TPath>,
{ method: TMethod; params: TParams; queries: TQueries },
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Avoid as unknown as casts by preserving segment types when splitting. Use a typed helper to narrow non-empty segments so the returned tuple aligns with PathSegments<TPath> at runtime and compile-time. [possible issue, importance: 4]

Suggested change
const segments = path.split("/").filter((segment) => segment.length > 0);
return [
...segments,
{
method: options.method,
params: options.params,
queries: options.queries,
},
] as unknown as readonly [
...PathSegments<TPath>,
{ method: TMethod; params: TParams; queries: TQueries },
];
const isNonEmpty = (s: string): s is Exclude<string, ""> => s.length > 0;
const segments = path.split("/").filter(isNonEmpty);
return [
...segments,
{
method: options.method,
params: options.params,
queries: options.queries,
},
];

Comment on lines +77 to +95
const { url, method, res, queryKey } = ApiURL.create(
"/api/leaderboard/current/user/all",
{
method: "GET",
queries: {
page,
pageSize,
filters,
globalIndex,
query: debouncedQuery,
}),
globalIndex,
...Object.typedFromEntries(
Object.typedEntries(filters).map(([tagEnum, filterEnabled]) => {
const metadata = ApiUtils.getMetadataByTagEnum(tagEnum);

return [metadata.apiKey, filterEnabled];
}),
),
},
},
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Guard against undefined filters to prevent runtime errors when calling Object.typedEntries. Default to an empty object or short-circuit the spread to ensure stability. [possible issue, importance: 3]

Suggested change
const { url, method, res, queryKey } = ApiURL.create(
"/api/leaderboard/current/user/all",
{
method: "GET",
queries: {
page,
pageSize,
filters,
globalIndex,
query: debouncedQuery,
}),
globalIndex,
...Object.typedFromEntries(
Object.typedEntries(filters).map(([tagEnum, filterEnabled]) => {
const metadata = ApiUtils.getMetadataByTagEnum(tagEnum);
return [metadata.apiKey, filterEnabled];
}),
),
},
},
);
const safeFilters = filters ?? {};
const { url, method, res, queryKey } = ApiURL.create(
"/api/leaderboard/current/user/all",
{
method: "GET",
queries: {
page,
pageSize,
query: debouncedQuery,
globalIndex,
...Object.typedFromEntries(
Object.typedEntries(safeFilters).map(([tagEnum, filterEnabled]) => {
const metadata = ApiUtils.getMetadataByTagEnum(tagEnum);
return [metadata.apiKey, filterEnabled];
}),
),
},
},
);

Comment on lines +10 to +15
if (data?.success && data.payload.lobby.joinCode) {
navigate("/duel/current");
} else {
navigate("/duel");
}
}, [data?.success, navigate]);
}, [data?.payload?.lobby.joinCode, data?.success, navigate]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Optional-chain payload and lobby in the condition to match the dependency array and avoid possible undefined access. This prevents runtime errors when data.success is true but payload or lobby is missing. [possible issue, importance: 6]

Suggested change
if (data?.success && data.payload.lobby.joinCode) {
navigate("/duel/current");
} else {
navigate("/duel");
}
}, [data?.success, navigate]);
}, [data?.payload?.lobby.joinCode, data?.success, navigate]);
useEffect(() => {
if (data?.success && data?.payload?.lobby?.joinCode) {
navigate("/duel/current");
} else {
navigate("/duel");
}
}, [data?.payload?.lobby?.joinCode, data?.success, navigate]);

@github-actions
Copy link
Contributor

Overall Project 77.16% 🍏

There is no coverage information present for the Files changed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants