Skip to content

fix: paginate listEntries with scroll#3384

Merged
hellofanny merged 1 commit into
3.xfrom
paginate-list-entries-3x
Jun 11, 2026
Merged

fix: paginate listEntries with scroll#3384
hellofanny merged 1 commit into
3.xfrom
paginate-list-entries-3x

Conversation

@vitorflg

@vitorflg vitorflg commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

What's the purpose of this pull request?

ContentService.getPage was returning only the first page of entries from Content Platform because listEntries wasn't following the scroll token. Pages backed by CP silently missed content beyond the first batch.

How it works?

In packages/core/src/server/content/service.ts, the single listEntries call is replaced by a loop that accumulates entries while a scroll token is returned, then passes the full list to fillEntriesWithData.

How to test it?

  • On a store using the CP data source, create enough entries for a given content type to exceed a single page.
  • Request that page and verify all entries are returned (not just the first batch).
  • Confirm non-CP (hCMS) path is unaffected.

Starters Deploy Preview

N/A

References

Cherry-pick of 2e43804 from dev into 3.x.

Type of Change

  • 🍕 New feature
  • 🐛 Bug fix
  • 🔥 Breaking change
  • 🤖 Build/CI
  • 📝 Documentation
  • ♻️ Refactor

Summary by CodeRabbit

  • Chores

    • Updated core client dependency to latest version for enhanced compatibility
  • Refactor

    • Optimized content retrieval logic with improved pagination support and streamlined single-item lookup to enhance performance

`ContentService.getPage` was returning only the first page of entries
from Content Platform because `listEntries` wasn't following the
`scroll` token. Result: pages backed by CP silently missed content
beyond the first batch.

In `packages/core/src/server/content/service.ts`, the single
`listEntries` call is replaced by a loop that accumulates entries while
a `scroll` token is returned, then passes the full list to
`fillEntriesWithData`.

- On a store using the CP data source, create enough entries for a given
content type to exceed a single page.
- Request that page and verify all entries are returned (not just the
first batch).
- Confirm non-CP (hCMS) path is unaffected.

N/A

N/A

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

* **Bug Fixes**
* Content retrieval now fully paginates via scrolling, accumulating
entries across pages and avoiding truncated results (stops safely with a
max-page cap and logs a warning if reached).

* **Enhancements**
* Product listing (PLP) and product detail (PDP) now attempt a direct
single-item fetch first and return immediately on direct matches,
improving accuracy and performance.

* **Notes**
  * No changes to public APIs or exported entities.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/vtex/faststore/pull/3282?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@vitorflg vitorflg requested a review from a team as a code owner June 9, 2026 12:58
@vitorflg vitorflg requested review from ommeirelles and renatomaurovtex and removed request for a team June 9, 2026 12:58
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

ContentService content retrieval is enhanced with a client library upgrade, scroll-based pagination capped at 25 pages for multi-entry fetches, and optimized direct-match fast paths in PLP and PDP methods that fall back to template selection when no direct match is found.

Changes

Content Fetching Enhancement

Layer / File(s) Summary
Client Library Update
packages/core/package.json
@vtex/client-cp dependency is upgraded from 0.3.5 to 0.6.0 to enable pagination and new content-fetching capabilities.
Scroll-Based Pagination in Multi-Entry Retrieval
packages/core/src/server/content/service.ts
getMultipleContent implements a scroll loop capped at MAX_PAGES = 25, accumulates all ContentEntry items across pages, warns if the pagination cap is reached, then passes accumulated entries to fillEntriesWithData.
Direct-Match Optimization in PLP and PDP
packages/core/src/server/content/service.ts
Both getPlpContent and getPdpContent attempt a fast single-content fetch via getSingleContent; if a match is found, both return immediately; otherwise, both fall back to the paginated multi-entry flow with template selection.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • vtex/faststore#3282: Implements identical pagination and direct-match optimization logic in ContentService, making the changes functionally overlapping.

Suggested reviewers

  • renatomaurovtex
  • mateuspontes

Poem

📜 Pagination scrolls through endless content streams,
Twenty-five pages dream the maximum dreams,
Direct matches leap—no templates to wait,
Fast paths and fallbacks keep requests first-rate! ⚡

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title 'fix: paginate listEntries with scroll' directly reflects the main technical change: implementing pagination with scroll-based looping for the listEntries call in ContentService.

✏️ 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 paginate-list-entries-3x

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.

@codesandbox-ci

codesandbox-ci Bot commented Jun 9, 2026

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@renatomaurovtex renatomaurovtex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cherry-pick into 3.x. The pagination loop is solid and the getSingleContent short-circuit is well structured — getFromCP returns undefined on not-found (404 caught at service.ts:134), so the if (directMatch) fallback to getMultipleContent works as intended. A few things to confirm before merge.

packages/core/package.json / pnpm-lock.yaml

🟠 [deps] @vtex/client-cp 0.3.5 → 0.6.0 in a published package

This jumps three minor versions on a 0.x package, where minors can be breaking. The new code relies on the listEntries(params, scroll) 2nd arg and result.scroll (new surface). It also keeps calling getEntry/getEntryBySlug/previewEntry* — please confirm those signatures didn't change across 0.4–0.6 and that 0.6.0 is the intended published version. Lockfile is updated, good. Since this is a runtime dep on @faststore/core, a maintainer sign-off on the bump is worth noting in the PR body.

packages/core/src/server/content/service.ts

🟠 [behavior] directMatch now short-circuits the multi-template selection

const directMatch = await this.getSingleContent<PLPContentType>(plpParams)
if (directMatch) {
  return directMatch
}

Before this PR, PLP/PDP always went through getMultipleContentfindBestPLPTemplate(pages, slug, rewrites) / findBestPDPTemplate(pages, product). Now an exact-slug entry returned by getEntryBySlug wins and the rewrites/best-template logic is skipped entirely. For most stores "exact slug wins" is the desired behavior, but for stores relying on multiple templates + rewrites this is a behavior change. Can you add an explicit test step covering a store that has both an exact-slug entry and rewrite-matched templates, to confirm the exact one is meant to win? (product isn't used in the PDP fast path either — same question.)

💬 [behavior] Silent truncation past MAX_PAGES

if (pages >= MAX_PAGES) {
  console.warn('listEntries pagination hit MAX_PAGES cap', serviceParams)
}

The infinite-loop guard (next === scroll → break) and the cap are good defensive moves. Note that hitting the cap silently re-introduces the exact bug this PR fixes (missing entries beyond page 25). The TODO makes the temporary intent clear, so this is fine — just flagging that the warn is the only signal. Consider routing it through the diagnostics logger instead of console.warn (cf. the logger work in #3300).

🟡 [convention] Commit type

Title is feat: but the PR is checked as 🐛 Bug fix (and it's a fix cherry-picked into the 3.x maintenance branch). Type drives the release bump, so this should likely be fix(core): rather than feat:.

Verdict: Approved with comments

Blocking (🟠):

  • Confirm @vtex/client-cp 0.6.0 is intended and the getEntry*/previewEntry* calls still match its API across the 0.3→0.6 jump (note the runtime-dep bump for maintainer sign-off).
  • Confirm the directMatch short-circuit is intended to win over findBestPLPTemplate/findBestPDPTemplate + rewrites; add a test step for the multi-template case.

Non-blocking (🟡/💬):

  • feat:fix(core): to match the bug-fix intent / release bump.
  • MAX_PAGES truncation is silent; consider the diagnostics logger over console.warn.

Checks to confirm before merge: pnpm lint · build · pnpm turbo run test --filter=@faststore/core · pnpm size.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@packages/core/src/server/content/service.ts`:
- Around line 67-71: The code passes the entire accumulated allEntries list into
fillEntriesWithData which causes pages × entriesPerPage concurrent
getEntryData() calls; instead, limit concurrency by processing entries in
bounded batches or adding a concurrency limiter. Change the call-site or
fillEntriesWithData signature to accept a batchSize or concurrency option (e.g.,
batchSize/concurrency param) and then iterate over allEntries in chunks (or use
a p-limit style worker pool) so only N getEntryData() calls run in parallel;
reference the allEntries variable, fillEntriesWithData(), getEntryData(), and
serviceParams/options.isPreview when implementing batching or a concurrency
limiter. Ensure the final result order is preserved by collecting batch results
in sequence and returning the same aggregated array.
- Around line 61-65: The loop in listEntries can exit due to hitting MAX_PAGES
while scroll is still truthy, leaving allEntries incomplete and then continuing
to template selection; modify listEntries so that if pages >= MAX_PAGES and
scroll is still set you do not proceed with downstream selection: detect this
condition (pages >= MAX_PAGES && scroll) after the loop and either (a)
abort/return a clear error or truncated-result indicator to the caller (so
template selection is skipped) or (b) explicitly clear/complete the scroll and
fetch remaining pages before proceeding; reference allEntries, scroll,
MAX_PAGES, listEntries and serviceParams when making this change so template
selection code only runs on a complete result set.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4f39d2a8-53de-41fa-b4db-c789d786c461

📥 Commits

Reviewing files that changed from the base of the PR and between 27db589 and bef6d60.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
📒 Files selected for processing (2)
  • packages/core/package.json
  • packages/core/src/server/content/service.ts

Comment on lines +61 to +65
} while (scroll && ++pages < MAX_PAGES)

if (pages >= MAX_PAGES) {
console.warn('listEntries pagination hit MAX_PAGES cap', serviceParams)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't continue with truncated results after the page cap.

If this loop exits with scroll still set, allEntries is incomplete but the code still runs template selection on it. That recreates the original bug for large CP datasets, just at a higher threshold, and can pick the wrong PLP/PDP content.

Suggested change
-      if (pages >= MAX_PAGES) {
-        console.warn('listEntries pagination hit MAX_PAGES cap', serviceParams)
-      }
+      if (scroll) {
+        throw new Error(
+          `listEntries pagination exceeded MAX_PAGES (${MAX_PAGES})`
+        )
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} while (scroll && ++pages < MAX_PAGES)
if (pages >= MAX_PAGES) {
console.warn('listEntries pagination hit MAX_PAGES cap', serviceParams)
}
} while (scroll && ++pages < MAX_PAGES)
if (scroll) {
throw new Error(
`listEntries pagination exceeded MAX_PAGES (${MAX_PAGES})`
)
}
🤖 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 `@packages/core/src/server/content/service.ts` around lines 61 - 65, The loop
in listEntries can exit due to hitting MAX_PAGES while scroll is still truthy,
leaving allEntries incomplete and then continuing to template selection; modify
listEntries so that if pages >= MAX_PAGES and scroll is still set you do not
proceed with downstream selection: detect this condition (pages >= MAX_PAGES &&
scroll) after the loop and either (a) abort/return a clear error or
truncated-result indicator to the caller (so template selection is skipped) or
(b) explicitly clear/complete the scroll and fetch remaining pages before
proceeding; reference allEntries, scroll, MAX_PAGES, listEntries and
serviceParams when making this change so template selection code only runs on a
complete result set.

Comment on lines +67 to +71
return this.fillEntriesWithData(
allEntries,
serviceParams,
options.isPreview
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound the CP enrichment fan-out before passing all pages downstream.

This now turns a single request into pages × entriesPerPage parallel getEntryData() calls through fillEntriesWithData(). On large catalogs that can blow the request budget or throttle CP hard. Please batch page enrichment or add a concurrency limit instead of sending the whole accumulated list at once.

🤖 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 `@packages/core/src/server/content/service.ts` around lines 67 - 71, The code
passes the entire accumulated allEntries list into fillEntriesWithData which
causes pages × entriesPerPage concurrent getEntryData() calls; instead, limit
concurrency by processing entries in bounded batches or adding a concurrency
limiter. Change the call-site or fillEntriesWithData signature to accept a
batchSize or concurrency option (e.g., batchSize/concurrency param) and then
iterate over allEntries in chunks (or use a p-limit style worker pool) so only N
getEntryData() calls run in parallel; reference the allEntries variable,
fillEntriesWithData(), getEntryData(), and serviceParams/options.isPreview when
implementing batching or a concurrency limiter. Ensure the final result order is
preserved by collecting batch results in sequence and returning the same
aggregated array.

@vitorflg vitorflg changed the title feat: paginate listEntries with scroll fix: paginate listEntries with scroll Jun 9, 2026
@hellofanny hellofanny merged commit 83fdae8 into 3.x Jun 11, 2026
8 of 9 checks passed
@hellofanny hellofanny deleted the paginate-list-entries-3x branch June 11, 2026 13:14
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.

3 participants