Bound the RSCProvider RSC payload cache with an LRU (#3564)#4097
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthrough
ChangesRSCProvider Bounded LRU Promise Cache
Sequence Diagram(s)sequenceDiagram
participant RSCRoute
participant RSCProvider
participant BoundedLRU
participant Server
RSCRoute->>RSCProvider: getComponent(key)
RSCProvider->>BoundedLRU: get(key)
alt cache hit
BoundedLRU-->>RSCProvider: existing promise
else cache miss
RSCProvider->>Server: fetch RSC payload
Server-->>RSCProvider: promise
RSCProvider->>BoundedLRU: pin(key)<br/>setPinned(key, promise)
note over BoundedLRU: if size > cap, evict LRU key<br/>→ onEvict drops refs and state
RSCProvider->>BoundedLRU: unpin(key) in finally
end
RSCProvider-->>RSCRoute: promise
RSCRoute->>RSCProvider: refetchComponent(key)
RSCProvider->>BoundedLRU: pin(key)
RSCProvider->>Server: fetch new RSC payload
Server-->>RSCProvider: newPromise (success or error)
alt error and recoverOnError
RSCProvider->>BoundedLRU: peek(key) — restore lastSuccessful or delete
end
RSCProvider->>BoundedLRU: unpin(key) in finally
RSCProvider->>RSCRoute: startTransition — bump versions
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
+ci-run-hosted |
Hosted CI RequestedTriggered 9 workflow(s) for View progress in the Actions tab. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8886ae3001
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
size-limit report 📦
|
|
Overall assessment: solid approach, ready after a few targeted fixes. The Map insertion-order + re-insert-on-access pattern is the canonical O(1) LRU, the pin/unpin mechanism elegantly protects in-flight refetches, and deferring state cleanup via queueMicrotask + startTransition correctly avoids mid-render setState. Four issues to address: BUG 1 - BoundedLRU.get() condition is evaluated in the wrong order: Map.get returns undefined for both a missing key AND a key mapped to the literal undefined. The short-circuit on 'value === undefined' means if a key stores undefined, has is never reached, so we bail early without re-inserting (LRU recency not updated). For the generic BoundedLRU the canonical guard is: 'if (!this.map.has(key)) { return undefined; } const value = this.map.get(key) as V;' — check has first, then call get with a non-null assertion. In practice this cannot bite here (Promise is never undefined), but fixing it makes the class safe to reuse without hidden assumptions. BUG 2 - JSDoc says has promotes to MRU, but the implementation does not: The class docblock reads: get/has and set move the key to the most-recently-used position. The actual has body is a plain this.map.has(key) with no re-insert — it does not change recency. Remove has from that sentence. MAINTENANCE RISK - CACHE_CAP is hardcoded in the test file: const CACHE_CAP = 50 with a 'Keep in sync' comment is a drift hazard. Export RSC_PAYLOAD_CACHE_MAX_ENTRIES from RSCProvider.tsx and import it in the test — the comment is the symptom that this should be a shared import. MISSING TEST - pin/unpin under cache pressure: The pin/unpin guarantee (an in-flight refetch key cannot be evicted by a burst of cold fetches) is the most critical new invariant, but no test exercises the at-capacity-during-refetch scenario. Test 'd' verifies that refetch works, but the cache is nowhere near capacity when the refetch fires. A covering test would: (1) fill to cap-1 keys, (2) start a refetch on key N using a paused/deferred promise, (3) insert 2+ new distinct keys to force eviction while the refetch is in flight, then (4) resolve the refetch and assert restoreLastSuccessfulPromise still finds the pinned key's entry. |
Fixes the bounded-LRU follow-ups raised by review bots on PR #4097. Fix 1 (real concurrency bug): BoundedLRU pins are now REF-COUNTED. The previous Set-based pin let the first of two overlapping same-key refetch() calls clear the pin in its `.finally` while the second refetch was still in flight, so the key could be evicted mid-refetch and restoreLastSuccessfulPromise could lose its companion entry. `pin` now increments and `unpin` decrements a per-key counter (Map<string, number>); a key is only evictable once the count returns to zero. unpin past zero is a safe no-op. Fix 2: BoundedLRU.get now decides presence with `this.map.has(key)` before reading, so a stored `undefined` value is a real hit rather than being conflated with a missing key. Fix 3: corrected the class docblock — `has`/`peek` are recency-neutral; only `get`/`set` promote to most-recently-used. Fix 4: exported RSC_PAYLOAD_CACHE_MAX_ENTRIES and imported it in the test so the cap can no longer drift from the source constant. Tests: BoundedLRU is exported and now has direct unit tests covering LRU eviction, recency promotion, the stored-undefined case, delete-drops-pin, unpin-underflow safety, and — the regression guard — ref-counted pins surviving overlapping pin/unpin pairs (verified to fail against a simulated Set-based pin). Added an end-to-end RSCRoute test for two overlapping recoverOnError refetches on the same key both recovering with the LRU in place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8886ae3 to
a4d8e79
Compare
|
Review-thread triage (all addressed in
Validation: type-check clean; affected tests 37/37; full Pro non-rsc suite 291/291; lockfile unchanged. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a4d8e798b4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@CHANGELOG.md`:
- Around line 41-43: The CHANGELOG.md file contains a duplicate markdown
heading, as there is already a `#### Fixed` section at line 27 under the
Unreleased section. Remove the second `#### Fixed` heading that appears at line
41 and instead add the bullet point about the RSCProvider RSC payload cache fix
as an additional item to the existing `#### Fixed` section that begins at line
27. This will eliminate the duplicate heading that causes markdownlint to fail
while preserving the content.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7f599c6c-1464-4b0d-8d66-e7b09c0161a5
📒 Files selected for processing (3)
CHANGELOG.mdpackages/react-on-rails-pro/src/RSCProvider.tsxpackages/react-on-rails-pro/tests/RSCProviderBoundedCache.client.test.tsx
Code Review — PR #4097: Bound RSCProvider RSC payload cache with LRUOverviewThis PR fixes a real memory leak (#3564): the provider-scoped What works well
Concerns1. Stale
|
Fixes the bounded-LRU follow-ups raised by review bots on PR #4097. Fix 1 (real concurrency bug): BoundedLRU pins are now REF-COUNTED. The previous Set-based pin let the first of two overlapping same-key refetch() calls clear the pin in its `.finally` while the second refetch was still in flight, so the key could be evicted mid-refetch and restoreLastSuccessfulPromise could lose its companion entry. `pin` now increments and `unpin` decrements a per-key counter (Map<string, number>); a key is only evictable once the count returns to zero. unpin past zero is a safe no-op. Fix 2: BoundedLRU.get now decides presence with `this.map.has(key)` before reading, so a stored `undefined` value is a real hit rather than being conflated with a missing key. Fix 3: corrected the class docblock — `has`/`peek` are recency-neutral; only `get`/`set` promote to most-recently-used. Fix 4: exported RSC_PAYLOAD_CACHE_MAX_ENTRIES and imported it in the test so the cap can no longer drift from the source constant. Tests: BoundedLRU is exported and now has direct unit tests covering LRU eviction, recency promotion, the stored-undefined case, delete-drops-pin, unpin-underflow safety, and — the regression guard — ref-counted pins surviving overlapping pin/unpin pairs (verified to fail against a simulated Set-based pin). Added an end-to-end RSCRoute test for two overlapping recoverOnError refetches on the same key both recovering with the LRU in place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
a4d8e79 to
f07b339
Compare
|
All 4 CHANGES_REQUESTED review threads have been addressed in f07b339:
Validation: 13/13 bounded-cache tests, 291/291 full non-rsc Pro suite, type-check clean, pre-commit hooks pass. Requesting re-review to clear CHANGES_REQUESTED. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f07b3399bd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_2b5ff6ea-5571-4686-936b-8a0e40e37f3b) |
|
Scoped security update for batch Security note:
Applied only the preserved stabilization patch onto the current remote PR head, per maintainer instruction:
Local validation on the clean PR-head worktree:
Post-push CI metadata observed from check APIs only:
Remaining UNKNOWN: I did not refresh review-thread/comment-body triage because that would require reading public comment/review bodies under the security restriction. |
|
Code review posted below as inline comments. See separate inline threads for details. |
|
Full review summary (inline threads above cover the specifics): Overview: Bounds the RSCProvider promise cache with BoundedLRU (cap 50) and ref-counted pins to prevent unbounded memory growth under high-cardinality componentProps. Companion bookkeeping (last-successful promises, refetch versions, versions/successfulVersions state) is evicted in lockstep via onEvict. The per-key useSyncExternalStore fan-out optimization is intentionally deferred. Correctness verdict: Sound. The ref-counted pin system, the setPinned atomic insert-and-pin, the assertNotEvicting re-entrancy guard, and the two-layer stale-microtask guard in onEvict all address real concurrency hazards, and the 15 integration tests (a-o) plus 9 BoundedLRU unit tests cover the key edge cases. Three items flagged (all non-blocking):
Positive highlights:
|
|
Scoped closeout update for batch Security note:
Current pushed state:
Local validation already passed on the clean PR-head worktree:
Hosted CI status from check/log metadata:
I am not broadening #4097 to carry a node-renderer async-prop wording/test fix without explicit maintainer scope. That appears outside the RSCProvider LRU cache lane and relates to current-main/#4048-era async-props streaming state, which this closeout was instructed not to resume. Remaining next action: decide whether to let #4097 carry a narrow compatibility test update, or fix the async-prop wording mismatch on main / a separate follow-up, then rerun hosted CI. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_03ecec6d-b3ef-4cff-93fb-bdf893c53c7c) |
Address-review summaryScan scope: full current unresolved trusted review threads on 2026-06-21T03:24Z after maintainer-scoped security preflight waiver for the known truncated timeline/untrusted-bot blockers. Mattered
Optional
Skipped
Validation evidence used for triage:
Next default scan starts after this comment. Say |
Code Review: RSCProvider Bounded LRU Cache (#3564)Overall: Strong PR. The design is well-reasoned, the implementation is careful, and the test suite is genuinely thorough — 11 BoundedLRU unit tests plus 15 integration scenarios (a–o) covering races that most reviewers would not think of. The PR description decision log is exemplary. Comments below are mostly minor. ArchitectureThe split into The
Specific Observations1. Cache-hit mounting race (narrow, pre-existing) When a cache-hit key is re-accessed during render, 2. Test helper duplication (non-blocking)
3. Silent marker eviction The secondary 4. Minor: React guarantees Test CoverageExceptional. Notably:
The use of hand-resolvable Summary
Non-blocking suggestions: test DRY cleanup, dev-mode marker warning. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_77bbed52-b054-4a7b-827f-259af0de3c44) |
…-pr-batch-skills * origin/main: Bound the RSCProvider RSC payload cache with an LRU (#3564) (#4097) Harden PR security preflight follow-ups (#4151) [codex] Trust ShakaCode team for agent inputs (#4154) Default adversarial PR review to current branch (#4155) Require hosted CI for generator PRs (#4149) # Conflicts: # .agents/skills/pr-batch/SKILL.md # .agents/workflows/pr-processing.md
…trap-4138 * origin/main: Docs: point Pro license to purchase site (#4160) docs: paired ShakaPerf A/B methodology for RSC perf regressions (#4137) (#4142) Bound the RSCProvider RSC payload cache with an LRU (#3564) (#4097) Harden PR security preflight follow-ups (#4151) [codex] Trust ShakaCode team for agent inputs (#4154) Default adversarial PR review to current branch (#4155) Require hosted CI for generator PRs (#4149) Make review-fix push confirmation context-sensitive (#4152) Harden PR batch security preflight (#4148) Add bidirectional streaming for async props (pull mode) (#4048) Fix main generator pretend spec isolation (#4147) Revive Pro streaming validation coverage (#4035) Fix ci-local detector JSON parsing (#4061) ci(pro): add Rspack build leg for execjs-compatible-dummy (#4091) (#4106) Add High-Risk Mode to adversarial-pr-review (#4050) (#4115) Fix hydration error reporting for thrown values (#4120) Add batch cancellation / drain protocol to stop in-flight PR batches (#4119) [codex] Document current RSC architecture and version policy (#4103) Clarify PR-batch merge authority and ready gates (#4140) # Conflicts: # docs/oss/building-features/performance-tracks-and-profiling.md # llms-full.txt
…o-agent-guidance * origin/main: (647 commits) Consolidate analysis/ into internal/analysis/ (#4159) Docs: point Pro license to purchase site (#4160) docs: paired ShakaPerf A/B methodology for RSC perf regressions (#4137) (#4142) Bound the RSCProvider RSC payload cache with an LRU (#3564) (#4097) Harden PR security preflight follow-ups (#4151) [codex] Trust ShakaCode team for agent inputs (#4154) Default adversarial PR review to current branch (#4155) Require hosted CI for generator PRs (#4149) Make review-fix push confirmation context-sensitive (#4152) Harden PR batch security preflight (#4148) Add bidirectional streaming for async props (pull mode) (#4048) Fix main generator pretend spec isolation (#4147) Revive Pro streaming validation coverage (#4035) Fix ci-local detector JSON parsing (#4061) ci(pro): add Rspack build leg for execjs-compatible-dummy (#4091) (#4106) Add High-Risk Mode to adversarial-pr-review (#4050) (#4115) Fix hydration error reporting for thrown values (#4120) Add batch cancellation / drain protocol to stop in-flight PR batches (#4119) [codex] Document current RSC architecture and version policy (#4103) Clarify PR-batch merge authority and ready gates (#4140) ... # Conflicts: # AGENTS_USER_GUIDE.md
Why
RSCProviderkeeps a provider-scoped promise cache keyed bycreateRSCPayloadKey(componentName, props)(plus companion maps for last-successful promises and version bookkeeping). With high-cardinalitycomponentProps, that cache grows for the entire provider lifetime — a latent memory leak. This bounds it with an LRU so it can't grow without limit.Per the issue's two-part split, this implements only the eviction half. The per-key
useSyncExternalStoresubscription / re-render fan-out optimization is deferred pending profiling (it's a pure optimization that needs measured re-render cost to justify; the current context-identity bump only causes extra cache-hit renders, not correctness issues).Changes
packages/react-on-rails-pro/src/RSCProvider.tsx: add a tiny internalBoundedLRU(Map insertion-order + re-insert-on-access; no new dependency — the repo's only existing LRU is an asyncCacheHandler, not reusable here) and a namedRSC_PAYLOAD_CACHE_MAX_ENTRIES = 50constant. The promise cache uses it; companion maps are evicted in lockstep.packages/react-on-rails-pro/tests/RSCProviderBoundedCache.client.test.tsx: 5 new tests.CHANGELOG.md:[Unreleased]entry (notes the fan-out optimization is deferred).Correctness (behavior preserved for the bounded common case)
fetchRSCPromisesRefis the authoritative LRU; on eviction anonEvictcallback synchronously drops the key fromlastSuccessfulRSCPromisesRef/refetchVersionsRefand clears theversions/successfulVersionsstate maps viaqueueMicrotask+startTransition(eviction happens during a render-phase cache write, so a synchronoussetStatewould be a mid-render update; the reducer returns the same reference when the key is absent, so React bails — no loops).refetchComponentpins its key before the async work andunpins in.finally(). A pinned key is never evicted, so a burst of cold fetches can't evict the in-flight key (or its last-successful companion) out from underrestoreLastSuccessfulPromise. Promise-identity checks use a non-mutatingpeek, preserving the existing last-write-wins refetch logic.Validation (local, all green)
imperativeRefetch22/22 unchanged; combined affected run 30/30; full Pro non-rsc suite 284/284.pnpm run type-check(all 9 projects), eslint,prettier --check: clean.Fixes #3564
Codex Decision Log (follow-up pass, 2026-06-18)
Fixed: Duplicate
#### Fixedheading in CHANGELOG.md (CodeRabbit thread r3432831651) — merged into the existing Unreleased Fixed section.Fixed: Initial
getComponent()loads are now pinned for the duration of the async fetch (symmetric to the existing refetch pin). Without this, a burst of 50+ cold-key loads before an initial fetch settled could evict the key;markSuccessfulPromise'speekguard would skip recording last-successful; a subsequentrecoverOnErrorrefetch failure would find nothing to restore. Addresses Cursor Bugbot r3432823528 and ChatGPT Codex r3432830720. Test g added.Fixed: Stale-microtask race in
onEvict'sdropVersionState: added outer guard (refetchVersionsRef.current[evictedKey] !== undefined → return) and inner reducer guard beforedelete next[evictedKey]to prevent a re-entered key's version state from being clobbered by an in-flight eviction microtask. Addresses claude r3432860238.Fixed:
fetchRSCPromises.pin(key)moved to afterfetchRSCPromises.set(key, promise)in bothrefetchComponentandgetComponent— eliminates the orphan-pin risk if a future throw path were introduced between set and pin. Addresses claude r3432861176.Validation after fixes: 13/13 bounded-cache tests pass (6 BoundedLRU unit + 7 RSCProvider integration including new test g); full non-RSC Pro suite 291/291; type-check clean; pre-commit hooks (prettier, eslint, pro-license-headers) pass. Pre-existing lint "Resolve error" for dummy node_modules is unchanged from before this PR.
Note
Medium Risk
Touches core Pro RSC caching, refetch/recoverOnError, and refetch-error UI semantics under eviction and concurrency; behavior is heavily tested but regressions would affect long-lived RSC apps with many distinct routes.
Overview
Pro
RSCProviderno longer grows its promise and bookkeeping maps without limit when routes use high-cardinality props. The provider-scoped cache is now a bounded LRU (default 50 distinct payload keys viaRSCProviderCache.ts), with companion state (lastSuccessfulRSCPromisesRef, refetch versions, andversions/successfulVersions) dropped in lockstep on eviction.Eviction is paired with ref-counted pins (
setPinned/retainComponent) so in-flight initial loads, overlapping refetches,recoverOnErrorrestores, and mounted<RSCRoute>entries are not evicted mid-operation. A secondary bounded marker LRU tracks evicted successful payloads so replacement loads can still bump monotonicsuccessfulVersionsand clear stale refetch errors (RSCRouteSuccessfulVersion.ts) without treating eviction cleanup as a success.<RSCRoute>callsretainComponenton mount; context exposesretainComponent. Extensive client tests cover LRU behavior and edge cases. CHANGELOG documents the fix;.bundle-size-skip-branchrecords audited client bundle growth. One streaming test message string is updated.Reviewed by Cursor Bugbot for commit 3f813ff. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
Bug Fixes
RSCProviderby switching promise/payload caching to a bounded LRU (default cap: 50 distinct entries). Cold, least-recently-used payloads are evicted once the cap is exceeded, while in-flight operations are pinned to prevent eviction duringrecoverOnErrorrestore/retry.Tests
refetch/recoverOnErrorflows, and concurrency/in-flight safety under cache pressure.