Skip to content

Solving N+1 problems with batch requests#2390

Merged
GelatoGenesis merged 2 commits into
mainfrom
b-17787499623
May 14, 2026
Merged

Solving N+1 problems with batch requests#2390
GelatoGenesis merged 2 commits into
mainfrom
b-17787499623

Conversation

@GelatoGenesis
Copy link
Copy Markdown
Collaborator

@GelatoGenesis GelatoGenesis commented May 14, 2026

Summary by CodeRabbit

  • Improvements

    • Faster, more efficient drop loading via batched requests, preserving order and reducing latency.
    • More reliable drop previews by serial number with stricter wave matching, improving link/quote accuracy.
  • New Features

    • Quote component now fetches drop previews directly for more accurate, up-to-date display.
  • Chores

    • API schema and endpoint handling updated.
  • Tests

    • Expanded and hardened test coverage for batching, chunking, deduplication, and abort behavior.

Review Change Stack

Signed-off-by: GelatoGenesis <tarmokalling@gmail.com>
simo6529
simo6529 previously approved these changes May 14, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

📝 Walkthrough

Walkthrough

This PR migrates drop lookups to batched V2 endpoints (ids and serial_nos), adds server schema/router wiring and client hydration, implements client-side serial-number micro-batching with chunking and abort handling, refactors drop-api to batch and chunk ID requests, and updates components/tests to the new query shapes.

Changes

Drop Fetching V2 Batch Migration

Layer / File(s) Summary
OpenAPI schema and type updates
openapi.yaml, contexts/wave/hooks/types.ts
Adds serial_nos/ids query parameters to drops endpoints, adds x-6529-router wiring across multiple v2 routes, extends ApiDropV2ContextProfileContext with wave, adds WaveMessagesUpdate type, and removes the WaveDropsSearchStrategy re-export.
V2 drop batch fetching with embedded wave hydration
services/api/wave-drops-v2-api.ts, __tests__/services/api/wave-drops-v2-api.test.ts
Introduces fetchDropsV2ByIds and FetchDropsV2ByIdsProps; requests v2/drops with comma-joined ids and page_size, then hydrates ApiDropV2 results that include embedded wave into ApiDrop objects, filtering out failed/malformed entries.
Drop API batch integration with chunking
services/api/drop-api.ts, __tests__/services/api/drop-api.test.ts
Reworks fetchDropResultsByIds to deduplicate IDs, chunk into batches (100), call fetchDropsV2ByIds per chunk with lean hydration flags, and convert Promise.allSettled batch outcomes into per-id results with synthesized missing-ID errors.
Quorum participation serial-number batching
services/api/quorum-participation-drop-preview-v2-api.ts, __tests__/services/api/quorum-participation-drop-preview-v2-api.test.ts
Implements a serialPreviewBatcher that queues serial-number preview requests, dedupes and chunks serials, issues batched v2/drops calls with serial_nos and page_size, maps responses to ApiDrop (guarding on wave.id), supports per-request aborts, and exposes tests for batching, deduplication, chunking, wave mismatch, and abort behavior.
Component refactor: WaveDropQuoteWithSerialNo and link preview tests
components/waves/drops/WaveDropQuoteWithSerialNo.tsx, __tests__/components/waves/WaveDropQuoteWithSerialNo.test.tsx, __tests__/components/waves/drops/WaveDropLinkPreview.test.tsx
WaveDropQuoteWithSerialNo now queries fetchQuorumParticipationDropPreviewBySerialNoV2 via React Query under QueryKey.DROP with "wave-drop-quote-serial" namespace; tests updated to expect the new query key and v2 response shape ({ data: [drop] }) with embedded wave.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ragnep

Poem

"A rabbit batches serials in a flurry of hops,
Joins ids and numbers into tidy crop-tops,
Promises settle, chunks race through the night,
Waves and drops pair up, everything’s right,
Hooray — batched v2 fetches, compact and bright!" 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Solving N+1 problems with batch requests' accurately and concisely describes the main objective of the changeset, which introduces batch request mechanisms across multiple API modules to eliminate N+1 query problems.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch b-17787499623

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
services/api/wave-drops-v2-api.ts (2)

441-486: 💤 Low value

Consider logging failed hydration attempts.

The function silently filters out drops that fail hydration (lines 480-485), which could make it difficult to debug when drops don't appear as expected. While this defensive approach prevents partial failures from breaking the entire batch, it may hide legitimate errors (e.g., network issues, malformed data).

Consider adding error logging when hydration fails:

📊 Suggested enhancement
   const results = await Promise.allSettled(
     dropsWithWaves.map(({ drop, wave }) =>
       hydrateDropV2({
         drop,
         wave,
         signal,
         includeFullMetadata,
         includeTopRaters,
       })
     )
   );

   return results
     .filter(
       (result): result is PromiseFulfilledResult<ApiDrop> =>
-        result.status === "fulfilled"
+        {
+          if (result.status === "rejected") {
+            console.error("Failed to hydrate drop:", result.reason);
+            return false;
+          }
+          return true;
+        }
     )
     .map((result) => result.value);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/wave-drops-v2-api.ts` around lines 441 - 486, The current
hydrateDropsWithEmbeddedWavesV2 function swallows failed hydrateDropV2 results
by filtering only fulfilled promises; update the post-Promise.allSettled
handling to log rejected results so failures are visible for debugging: after
calling hydrateDropV2 for each drop, iterate the Promise.allSettled results, for
each result with status "rejected" log a contextual error (including the drop id
or wave id from the corresponding dropsWithWaves entry and the rejection reason)
using your service logger, then continue returning only the fulfilled values as
before; reference hydrateDropsWithEmbeddedWavesV2, hydrateDropV2 and the results
variable to locate where to add the logging.

626-651: ⚡ Quick win

Guard against malformed individual drop IDs.

Line 639 calls getNormalizedDropId which throws if an ID is empty after trimming. If dropIds contains even one whitespace-only or empty string, the entire batch request will fail.

Consider filtering out malformed IDs before the API call:

🛡️ Proposed defensive handling
 export async function fetchDropsV2ByIds({
   dropIds,
   signal,
   includeFullMetadata = false,
   includeTopRaters = false,
 }: FetchDropsV2ByIdsProps): Promise<ApiDrop[]> {
-  if (dropIds.length === 0) {
+  const normalizedIds = dropIds
+    .map((id) => id.trim())
+    .filter((id) => id.length > 0);
+
+  if (normalizedIds.length === 0) {
     return [];
   }

   const response = await commonApiFetch<ApiDropV2PageWithoutCount>({
     endpoint: "v2/drops",
     params: {
-      ids: dropIds.map(getNormalizedDropId).join(","),
-      page_size: dropIds.length.toString(),
+      ids: normalizedIds.join(","),
+      page_size: normalizedIds.length.toString(),
     },
     signal,
   });

   return hydrateDropsWithEmbeddedWavesV2({
     drops: response.data,
     signal,
     includeFullMetadata,
     includeTopRaters,
   });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/wave-drops-v2-api.ts` around lines 626 - 651, In
fetchDropsV2ByIds, guard against malformed IDs by filtering dropIds first (e.g.,
dropIds.filter(id => id && id.trim() !== "")) before calling getNormalizedDropId
so a whitespace/empty string doesn't make getNormalizedDropId throw; after
filtering, if the resulting list is empty return [] immediately, otherwise
proceed to map with getNormalizedDropId, call commonApiFetch and then
hydrateDropsWithEmbeddedWavesV2 as before.
services/api/drop-api.ts (1)

71-126: 💤 Low value

Remove non-null assertion with safer array access.

Line 93 uses a non-null assertion (chunks[index]!) which TypeScript requires because it can't verify forEach's index bounds. While this is safe in practice, using array destructuring in the forEach callback provides type safety without assertions:

♻️ Cleaner approach
-  chunkResults.forEach((result, index) => {
-    const chunk = chunks[index]!;
+  chunks.forEach((chunk, index) => {
+    const result = chunkResults[index];
+    if (!result) return;
+
     if (result.status === "rejected") {

Or keep the current structure but add a runtime guard:

   chunkResults.forEach((result, index) => {
-    const chunk = chunks[index]!;
+    const chunk = chunks[index];
+    if (!chunk) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/drop-api.ts` around lines 71 - 126, The non-null assertion
chunks[index]! in fetchDropResultsByIds should be removed; instead iterate
chunks with its index so you have a typed chunk value and match results by index
safely. Replace chunkResults.forEach((result, index) => { const chunk =
chunks[index]!; ... }) with chunks.forEach((chunk, index) => { const result =
chunkResults[index]; if (!result) { /* push rejected entries for chunk or
continue */ } ... }), or if you keep the original loop add a runtime guard const
chunk = chunks[index]; if (!chunk) { continue; } before using it to avoid the
assertion while preserving behavior.
components/waves/drops/WaveDropQuoteWithSerialNo.tsx (1)

43-47: 💤 Low value

Consider passing React Query's abort signal for future cancellation support.

React Query provides an AbortSignal via the query function context that you can forward to the fetch function. While the current implementation of fetchQuorumParticipationDropPreviewBySerialNoV2 ignores the signal parameter, adding it now would make the code cancellation-ready when the underlying API is fixed.

♻️ Proposed change to enable future cancellation
-    queryFn: async () =>
-      fetchQuorumParticipationDropPreviewBySerialNoV2({
+    queryFn: async ({ signal }) =>
+      fetchQuorumParticipationDropPreviewBySerialNoV2({
         waveId,
         serialNo,
+        signal,
       }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/waves/drops/WaveDropQuoteWithSerialNo.tsx` around lines 43 - 47,
The query function passed to React Query (queryFn) doesn't forward the provided
AbortSignal to fetchQuorumParticipationDropPreviewBySerialNoV2, so update
queryFn to accept the React Query context and pass context.signal into
fetchQuorumParticipationDropPreviewBySerialNoV2; also adjust
fetchQuorumParticipationDropPreviewBySerialNoV2 signature to accept an optional
signal param (and thread it into the underlying fetch) so the query can be
cancelled when React Query provides an AbortSignal.
__tests__/services/api/quorum-participation-drop-preview-v2-api.test.ts (1)

181-181: 💤 Low value

Simplify arrow function to direct Number reference.

The arrow function (serialNo) => Number(serialNo) is equivalent to passing Number directly as the map callback.

♻️ Proposed simplification
     const serialNos = request.params?.serial_nos
       .split(",")
-      .map((serialNo) => Number(serialNo));
+      .map(Number);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/services/api/quorum-participation-drop-preview-v2-api.test.ts` at
line 181, Replace the explicit arrow callback "(serialNo) => Number(serialNo)"
with a direct reference to the Number function in the map call to simplify the
code; locate the mapping expression ".map((serialNo) => Number(serialNo))" in
the test and change it to use ".map(Number)" so the array of serials is
converted to numbers more concisely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@__tests__/services/api/quorum-participation-drop-preview-v2-api.test.ts`:
- Around line 90-92: Tests leak module-level state because
pendingSerialPreviewRequests and isSerialPreviewBatchScheduled aren't reset; add
cleanup in the beforeEach to reset that state by either exporting a test-only
reset helper from the implementation (e.g., __resetBatchStateForTests that
clears pendingSerialPreviewRequests and sets isSerialPreviewBatchScheduled =
false) and calling it in beforeEach, or call jest.resetModules() and re-require
the module before each test to ensure pendingSerialPreviewRequests and
isSerialPreviewBatchScheduled are fresh; reference the module symbols
pendingSerialPreviewRequests, isSerialPreviewBatchScheduled and the proposed
__resetBatchStateForTests when making the change.

In `@services/api/quorum-participation-drop-preview-v2-api.ts`:
- Around line 22-26: The module-level mutable variables
pendingSerialPreviewRequests and isSerialPreviewBatchScheduled should be
encapsulated inside a class (e.g., SerialPreviewBatcher) instead of global
state: create a class that owns the Map and scheduled flag, provides methods to
enqueue requests, run scheduleSerialPreviewBatchFlush, and flush batches, and
export a singleton instance that caller code uses; add a resetForTests() method
on that class to clear state for test isolation and allow mocking/resetting
between tests; update any functions that reference pendingSerialPreviewRequests
or isSerialPreviewBatchScheduled to call the corresponding instance methods on
SerialPreviewBatcher (including scheduleSerialPreviewBatchFlush) so concurrent
contexts use the instance and state can be controlled in tests.
- Around line 91-125: flushSerialPreviewRequests currently does not support
cancellation so in-flight fetches cannot be aborted; change
flushSerialPreviewRequests to accept an AbortSignal (or create an
AbortController per batch if you want per-batch cancellation), thread that
signal into fetchSerialPreviewChunk, and ensure fetchSerialPreviewChunk forwards
the signal into commonApiFetch; also update the error handling in
flushSerialPreviewRequests to treat AbortError specially by rejecting queued
request promises with the abort error (or a consistent cancellation error) and
avoid retrying, and ensure callers that schedule batches pass their provided
signal through when enqueuing so component unmounts/caller cancellation actually
aborts the underlying network request (refer to symbols:
flushSerialPreviewRequests, fetchSerialPreviewChunk, commonApiFetch,
pendingSerialPreviewRequests, mapSerialPreviewDrop).

---

Nitpick comments:
In `@__tests__/services/api/quorum-participation-drop-preview-v2-api.test.ts`:
- Line 181: Replace the explicit arrow callback "(serialNo) => Number(serialNo)"
with a direct reference to the Number function in the map call to simplify the
code; locate the mapping expression ".map((serialNo) => Number(serialNo))" in
the test and change it to use ".map(Number)" so the array of serials is
converted to numbers more concisely.

In `@components/waves/drops/WaveDropQuoteWithSerialNo.tsx`:
- Around line 43-47: The query function passed to React Query (queryFn) doesn't
forward the provided AbortSignal to
fetchQuorumParticipationDropPreviewBySerialNoV2, so update queryFn to accept the
React Query context and pass context.signal into
fetchQuorumParticipationDropPreviewBySerialNoV2; also adjust
fetchQuorumParticipationDropPreviewBySerialNoV2 signature to accept an optional
signal param (and thread it into the underlying fetch) so the query can be
cancelled when React Query provides an AbortSignal.

In `@services/api/drop-api.ts`:
- Around line 71-126: The non-null assertion chunks[index]! in
fetchDropResultsByIds should be removed; instead iterate chunks with its index
so you have a typed chunk value and match results by index safely. Replace
chunkResults.forEach((result, index) => { const chunk = chunks[index]!; ... })
with chunks.forEach((chunk, index) => { const result = chunkResults[index]; if
(!result) { /* push rejected entries for chunk or continue */ } ... }), or if
you keep the original loop add a runtime guard const chunk = chunks[index]; if
(!chunk) { continue; } before using it to avoid the assertion while preserving
behavior.

In `@services/api/wave-drops-v2-api.ts`:
- Around line 441-486: The current hydrateDropsWithEmbeddedWavesV2 function
swallows failed hydrateDropV2 results by filtering only fulfilled promises;
update the post-Promise.allSettled handling to log rejected results so failures
are visible for debugging: after calling hydrateDropV2 for each drop, iterate
the Promise.allSettled results, for each result with status "rejected" log a
contextual error (including the drop id or wave id from the corresponding
dropsWithWaves entry and the rejection reason) using your service logger, then
continue returning only the fulfilled values as before; reference
hydrateDropsWithEmbeddedWavesV2, hydrateDropV2 and the results variable to
locate where to add the logging.
- Around line 626-651: In fetchDropsV2ByIds, guard against malformed IDs by
filtering dropIds first (e.g., dropIds.filter(id => id && id.trim() !== ""))
before calling getNormalizedDropId so a whitespace/empty string doesn't make
getNormalizedDropId throw; after filtering, if the resulting list is empty
return [] immediately, otherwise proceed to map with getNormalizedDropId, call
commonApiFetch and then hydrateDropsWithEmbeddedWavesV2 as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ac71210a-af2d-457f-ad10-75d038f7a09a

📥 Commits

Reviewing files that changed from the base of the PR and between 8335465 and 10f2e6e.

⛔ Files ignored due to path filters (2)
  • generated/models/ApiDropV2.ts is excluded by !**/generated/**
  • generated/models/ObjectSerializer.ts is excluded by !**/generated/**
📒 Files selected for processing (11)
  • __tests__/components/waves/WaveDropQuoteWithSerialNo.test.tsx
  • __tests__/components/waves/drops/WaveDropLinkPreview.test.tsx
  • __tests__/services/api/drop-api.test.ts
  • __tests__/services/api/quorum-participation-drop-preview-v2-api.test.ts
  • __tests__/services/api/wave-drops-v2-api.test.ts
  • components/waves/drops/WaveDropQuoteWithSerialNo.tsx
  • contexts/wave/hooks/types.ts
  • openapi.yaml
  • services/api/drop-api.ts
  • services/api/quorum-participation-drop-preview-v2-api.ts
  • services/api/wave-drops-v2-api.ts
💤 Files with no reviewable changes (1)
  • contexts/wave/hooks/types.ts

Comment thread services/api/quorum-participation-drop-preview-v2-api.ts Outdated
Comment thread services/api/quorum-participation-drop-preview-v2-api.ts Outdated
Signed-off-by: GelatoGenesis <tarmokalling@gmail.com>
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
services/api/quorum-participation-drop-preview-v2-api.ts (1)

246-259: 💤 Low value

Redundant error handling branches can be simplified.

Both the isAbortError branch and the fallthrough case call rejectRemainingRequests(). The early return after isAbortError check doesn't change behavior since both paths end up rejecting remaining requests. This could be simplified unless different behavior is planned for abort vs other errors.

♻️ Suggested simplification
     } catch (error) {
-      const rejectRemainingRequests = () => {
-        activeRequests.forEach((request) => {
-          request.reject(error);
-        });
-        activeRequests.clear();
-      };
-
-      if (isAbortError(error)) {
-        rejectRemainingRequests();
-        return;
-      }
-
-      rejectRemainingRequests();
+      activeRequests.forEach((request) => {
+        request.reject(error);
+      });
+      activeRequests.clear();
     } finally {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/quorum-participation-drop-preview-v2-api.ts` around lines 246 -
259, The catch block duplicates logic: both the isAbortError(error) branch and
the fallthrough call rejectRemainingRequests(), so remove the redundant
branch/early return and call rejectRemainingRequests() once. Locate the catch
block that defines rejectRemainingRequests (which iterates activeRequests and
calls request.reject(error) then clears activeRequests) and replace the
conditional that checks isAbortError(error) with a single unconditional call to
rejectRemainingRequests(); if special abort handling is intended later, add it
explicitly there instead of duplicating rejection logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@services/api/quorum-participation-drop-preview-v2-api.ts`:
- Around line 246-259: The catch block duplicates logic: both the
isAbortError(error) branch and the fallthrough call rejectRemainingRequests(),
so remove the redundant branch/early return and call rejectRemainingRequests()
once. Locate the catch block that defines rejectRemainingRequests (which
iterates activeRequests and calls request.reject(error) then clears
activeRequests) and replace the conditional that checks isAbortError(error) with
a single unconditional call to rejectRemainingRequests(); if special abort
handling is intended later, add it explicitly there instead of duplicating
rejection logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 193a65d0-604f-4a3c-8c96-71d5701a0145

📥 Commits

Reviewing files that changed from the base of the PR and between 10f2e6e and b910444.

📒 Files selected for processing (3)
  • __tests__/components/waves/drops/WaveDropLinkPreview.test.tsx
  • __tests__/services/api/quorum-participation-drop-preview-v2-api.test.ts
  • services/api/quorum-participation-drop-preview-v2-api.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/components/waves/drops/WaveDropLinkPreview.test.tsx
  • tests/services/api/quorum-participation-drop-preview-v2-api.test.ts

@GelatoGenesis GelatoGenesis merged commit 6b46ad2 into main May 14, 2026
8 checks passed
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