Skip to content

[Pro] Cap the size of external source maps read by the node renderer - #4688

Merged
justin808 merged 10 commits into
mainfrom
4313-node-renderer-external-source-maps-no-size-cap
Jul 17, 2026
Merged

[Pro] Cap the size of external source maps read by the node renderer#4688
justin808 merged 10 commits into
mainfrom
4313-node-renderer-external-source-maps-no-size-cap

Conversation

@AbanoubGhadban

@AbanoubGhadban AbanoubGhadban commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Partially addresses #4313.

Scope — please read first

#4313 is labeled parked/P3. In this comment @justin808 recommended no implementation PR for now, while allowing that a maintainer could "split a later narrow cap-only change with targeted source-map tests."

This PR is exactly that narrow cap-only change, and nothing more. It deliberately does not fix the issue's headline complaint. See "What this does not fix" below — that part is a product decision, not an oversight.

Problem

MAX_INLINE_SOURCE_MAP_BYTES (50MB) is referenced only in parseDataUrlSourceMap, so it guards data: URL maps only. readSourceMapFile and readSourceMapFileAsync read external .map files with no size check at all. A large external map is read in full and retained for the life of each pooled VM.

Change

External maps get the same 50MB ceiling as inline maps.

  • The size comes from the statSync that resolveReadableSourceMapPath already performs for its isFile() check, so the cap costs no extra syscall. resolveReadableSourceMapPath now returns { sourceMapPath, sizeInBytes }, and a new resolveSourceMapPathWithinSizeLimit applies the cap once for both the sync and async readers.
  • Oversized maps are skipped on both the async preload path (used by every buildVM) and the synchronous lazy path (used when a map lands after the bundle).
  • Warns once per map path. The async preload runs at every VM build and the sync path can run per error, so an unthrottled warning would flood the log on a rolling deploy.
  • 50MB matches the pre-existing inline cap rather than a stricter unevidenced value: making external maps stricter than inline ones, with no override, would be an unjustified regression risk for anyone with legitimately large maps.

Behavior change (intentional, called out in CHANGELOG)

Frames for a bundle whose external map exceeds 50MB keep their bundled locations instead of remapping to original sources, and the renderer logs a warning naming the map.

Honest limitation

This is a pre-read size gate, not a hard memory bound. A map that grows between the statSync and the readFileSync is still read in full. Stated in the code comment and CHANGELOG too — it should not be oversold as absolute protection.

What this does not fix

The issue's headline complaint is that a bundle which never errors still retains its full map size for the life of the pooled VM. This PR does not address that, and the obvious fix — dropping the eager preload so the existing lazy path runs — is provably wrong:

The existing test same-path rebuild does not remap active old VM errors with the new source map overwrites the .map on disk with a rebuilt map, then asserts that an old in-flight error still remaps to the original source. So the preload is not a cache — it retains the only surviving copy of that generation's map. A fingerprint ({size, mtimeMs, ino}) can detect staleness but cannot recover bytes that no longer exist on disk; it would return a raw stack and fail that assertion.

So removing the retention necessarily breaks a deliberately-encoded guarantee. The real tradeoff is:

  • today: correct stacks for in-flight errors on a replaced bundle, paid for with steady-state map retention
  • alternative: zero retention for zero-error bundles, paid for with un-remapped stacks in that window

That is a product call for @justin808, not one to make inside a cap PR. Left parked; I'll write it up on the issue.

Also deliberately excluded: drop-raw-JSON-after-parse (needs a "raw intentionally discarded" sentinel or a future parsed-map LRU would silently fall back to disk and reintroduce the stale-map risk the preload exists to prevent), the parsed-map LRU itself, and the extractSourceMappingUrl tail-scan.

Testing

Four targeted tests added (external source map size cap describe block):

  1. Oversized map on the async preload path → no remap, frame keeps bundle location, bytes never read (readFile/readFileSync asserted not called), warn logged.
  2. Oversized map on the sync lazy path (map lands after VM build) → same.
  3. Map exactly at the cap still remaps — boundary guard against a > / >= flip.
  4. Warn emitted once per map path across repeated lookups.

Verification performed:

  • 45/45 pass, including the two tests most at risk: external .map file next to the bundle is used (asserts the preload path does no sync read) and same-path rebuild does not remap active old VM errors with the new source map (generation isolation).
  • Vacuity check: with only the src change reverted and the tests intact, tests 1, 2 and 4 fail. Test 3 passes either way by design — it is a boundary guard, not a reproducer.
  • eslint clean; type-check shows 4 pre-existing errors (opentelemetry version skew + an unbuilt workspace dep), identical with the change reverted, none in vmSourceMapSupport.ts.

Planning, implementation, and review were done in a loop with Codex CLI (GPT-5.2, xhigh reasoning) as an adversarial peer. Codex refuted my original fingerprint-based design by finding the :895 test — that refutation is why this PR is cap-only.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Capped external .map loading in the Node renderer to 50 MB (matching inline limits). Oversized maps are skipped as terminal, no fallback/remap retries occur, and stack frames remain at bundled locations.
    • Logs a warning with cap/size details, deduped once per map path (bounded tracking).
  • Tests

    • Added Jest coverage for oversized-map behavior across preload (async) and lazy (sync) paths, including retry prevention and warning de-duplication.
  • Documentation

    • Updated the changelog to describe the 50 MB external source-map limit and its effects.

Completed-batch audit

Status: Follow-ups remain — see the durable receipt. Durable receipt.

AbanoubGhadban and others added 2 commits July 16, 2026 09:54
External `.map` files were read with no size check. `MAX_INLINE_SOURCE_MAP_BYTES`
(50MB) only guarded `data:` URL maps, so a large external map was read in full and
retained for the life of each pooled VM.

External maps now get the same 50MB ceiling as inline maps. The size comes from the
`statSync` that `resolveReadableSourceMapPath` already performs for its `isFile()`
check, so no extra syscall is added. An oversized map is skipped on both the async
preload path and the synchronous lazy path, and warns once per map path — the async
preload runs at every VM build and the sync path can run per error, so an unthrottled
warning would flood the log on a rolling deploy.

Behavior change: frames for a bundle whose external map exceeds the cap keep their
bundled locations instead of remapping to original sources.

This is a pre-read size gate, not a hard memory bound: a map that grows between the
stat and the read is still read in full.

Scope: implements only the narrow cap sanctioned in the issue discussion. It does not
fix the issue's headline complaint that bundles which never error still retain their
full map size. Removing that retention requires dropping the eager preload, which
provably breaks generation isolation — the "same-path rebuild does not remap active old
VM errors with the new source map" test overwrites the map on disk and then asserts an
old in-flight error still remaps to the original source, so the preload retains the only
surviving copy of that generation's map. A fingerprint can detect staleness but cannot
recover destroyed bytes. That tradeoff is a product decision and stays parked.

Partially addresses #4313.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The Node renderer caps external source-map reads at 50MB, skips oversized maps while preserving bundled stack locations, warns once per map path, and applies the behavior to synchronous and asynchronous loading.

Changes

External source map size cap

Layer / File(s) Summary
Implement external map size gating
packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts
External map resolution checks file size, skips maps over 50MB, logs bounded warnings, and resets warning state.
Propagate terminal oversized results
packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts
Synchronous and asynchronous discovery treat oversized maps as terminal, preventing fallback remapping and retries while preserving retry behavior for missing maps.
Validate capped map behavior
packages/react-on-rails-pro-node-renderer/tests/vmSourceMapSupport.test.ts, CHANGELOG.md
Tests cover preload and lazy loading, explicit-map fallback prevention, the exact size boundary, confirmed no-map registrations, non-retry behavior, warning de-duplication, and the documented release change.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Bundle
  participant NodeRenderer
  participant FileSystem
  participant Logger
  Bundle->>NodeRenderer: request stack remapping
  NodeRenderer->>FileSystem: inspect external .map size
  FileSystem-->>NodeRenderer: return map size
  NodeRenderer->>Logger: warn once when map exceeds 50MB
  NodeRenderer-->>Bundle: remap or preserve bundled locations
Loading

Possibly related issues

  • shakacode/react_on_rails#4313 — The change implements the issue’s external source-map size cap and terminal handling for sync and async loading.

Suggested labels: ready-for-hosted-ci

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: capping external source maps read by the Node renderer.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 4313-node-renderer-external-source-maps-no-size-cap

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.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR limits external source maps loaded by the Pro Node renderer. The main changes are:

  • Skips external source maps larger than 50 MB.
  • Keeps bundled stack locations when a map is too large.
  • Stops stale fallback maps from replacing an oversized named map.
  • Deduplicates warnings by map path.
  • Adds focused tests and changelog coverage.

Confidence Score: 5/5

This looks safe to merge.

  • The updated candidate ordering prevents an oversized named map from selecting a stale fallback.
  • A missing named map remains retryable when only the fallback is oversized.
  • The terminal state is handled consistently in preload and lazy lookup paths.
  • No blocking issues were found in the changed code.

Important Files Changed

Filename Overview
packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts Adds the external map size check and carries terminal versus retryable results through preload and lazy lookup.
packages/react-on-rails-pro-node-renderer/tests/vmSourceMapSupport.test.ts Covers oversized maps, candidate ordering, late map arrival, terminal state, and warning deduplication.
CHANGELOG.md Documents the external map limit and its effect on stack remapping.

Reviews (2): Last reviewed commit: "Do not let an oversized fallback map ret..." | Re-trigger Greptile

Comment thread packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f002782ef

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts Outdated
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review

Scope matches the description: this is a narrow, well-justified cap-only change (external .map files now share the 50MB ceiling already used for inline maps), reusing the statSync that resolveReadableSourceMapPath already performed so there's no added syscall on the hot path. The boundary test (size == cap still remaps) and the "no bytes read" assertions on both the async preload and sync lazy paths are good evidence this does what it claims.

Overview

  • resolveReadableSourceMapPath now returns { sourceMapPath, sizeInBytes } instead of just a path, letting the new resolveSourceMapPathWithinSizeLimit apply MAX_EXTERNAL_SOURCE_MAP_BYTES (== MAX_INLINE_SOURCE_MAP_BYTES, 50MB) without a second stat.
  • Oversized maps are skipped identically on both readSourceMapFile (sync/lazy) and readSourceMapFileAsync (async preload); frames keep their bundled location, warning logged once per path via warnedOversizedSourceMapPaths.
  • CHANGELOG entry clearly calls out the behavior change and the TOCTOU limitation (size can grow between statSync and the actual read). Good, honest documentation of the tradeoff.
  • 4 targeted new tests cover preload-path skip, lazy-path skip, exact-boundary pass-through, and warn-once dedup — and the PR description's vacuity check (tests fail with only src reverted) is a nice touch for confidence.

Minor observations (not blockers)

  1. warnedOversizedSourceMapPaths grows unboundedly for the process lifetime. Unlike sourceMapCache/missingSourceMapRetryCounts/etc., which get cleaned up in unregisterBundleForSourceMaps when a bundle is evicted, this new Set<string> (vmSourceMapSupport.ts:114) is never pruned outside of resetSourceMapSupport(), which is test-only (resetVM is @internal Used in tests). Over the life of a long-running renderer process across many rolling deploys, each distinct oversized map path (which typically includes a content hash) adds a permanent entry. Real-world impact is probably small since oversized maps should be rare, but it's a little ironic in a PR whose entire point is bounding per-VM memory retention — worth at least a comment acknowledging the tradeoff, or tying cleanup to unregisterBundleForSourceMaps.

  2. Oversized maps are funneled through the "map not yet uploaded" retry path, not treated as a distinct terminal state. In preloadSourceMapJsonForBundle, when the map is skipped for being oversized, readSourceMapJsonForBundleAsync returns undefined — indistinguishable from "map genuinely missing" — so retryMissingSourceMap: sourceMapJson === undefined becomes true. That means an oversized (permanently oversized, not just "hasn't landed yet") map gets re-stat'd on subsequent error-path lookups until MAX_MISSING_SOURCE_MAP_RETRIES (5) is hit, at which point retireMissingSourceMapRetry finally caches the miss. Functionally harmless (each retry is just a cheap re-stat, and the warning itself is correctly deduped), but it conflates two different failure reasons and does a bit of pointless retry churn for a condition that retrying can't fix. Could be tightened by having resolveSourceMapPathWithinSizeLimit (or its caller) surface "confirmed oversized" as a distinct outcome so preloadSourceMapJsonForBundle can set retryMissingSourceMap: false for that case specifically.

Neither of these affects correctness of the cap itself — both are steady-state efficiency/cleanliness nits. Nice, well-scoped change overall.

Comment thread packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts Outdated
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Summary

This is a well-scoped, well-documented change: it applies the existing 50MB inline-source-map cap (MAX_INLINE_SOURCE_MAP_BYTES) to external .map files too, reusing the statSync that resolveReadableSourceMapPath already performs (no extra syscall). The PR description is unusually thorough about tradeoffs and honest about limitations (pre-read gate, not a hard memory bound; TOCTOU window between statSync and the read). Tests cover the async preload path, the sync lazy path, the exact-boundary case, and warn-once-per-path dedup.

Strengths

  • Zero-cost size check (reuses existing statSync result via the new { sourceMapPath, sizeInBytes } return from resolveReadableSourceMapPath).
  • resolveSourceMapPathWithinSizeLimit cleanly centralizes the cap for both sync and async readers — no duplicated logic.
  • Warn-once-per-path (warnedOversizedSourceMapPaths) correctly prevents log flooding across the async preload (every buildVM) and sync lazy (every error) paths.
  • Good boundary test (size === cap still remaps, guards against a > vs >= flip).
  • CHANGELOG.md entry accurately describes the behavior change and its limitation.

Minor observations (non-blocking)

  1. Oversized maps are folded into the "retryable missing map" bucket. In preloadSourceMapJsonForBundle (vmSourceMapSupport.ts:492), retryMissingSourceMap: sourceMapJson === undefined is true for an oversized map, since readSourceMapJsonForBundleAsync returns undefined for both "not found" and "too large" (by design, per the JSDoc on resolveSourceMapPathWithinSizeLimit). This means a permanently-oversized map gets re-stat'd on the next several error lookups (up to MAX_MISSING_SOURCE_MAP_RETRIES = 5) before retireMissingSourceMapRetry settles it to a cached null. Cheap (only a stat, never a read) and self-healing, but worth confirming it's intentional rather than an oversight of collapsing two different states ("might arrive later" vs "will never fit") into one signal.
  2. Asymmetric warning behavior. parseDataUrlSourceMap (inline/data-URL maps) silently returns undefined when oversized with no log.warn, while this PR adds an explicit warn for external maps. Understandable given the PR's stated narrow scope, but it's a slight inconsistency in observability between the two cap sites that a future PR (or this one) could unify.
  3. Unbounded warnedOversizedSourceMapPaths Set (vmSourceMapSupport.ts:114) — only cleared in resetSourceMapSupport() (test-only). Since bundle paths appear to include a timestamp per deploy, a long-lived renderer process that repeatedly deploys an oversized bundle will accumulate one string entry per deploy for its entire lifetime. Negligible in practice, but noting it since nothing currently bounds or evicts it.

No correctness, security, or performance red flags found. The path-containment/realpath checks are unchanged, and the size check sits before the file read on both paths as intended.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 63.54 KB (0%)
react-on-rails/client bundled (gzip) (time) 63.54 KB (0%)
react-on-rails/client bundled (brotli) 54.55 KB (0%)
react-on-rails/client bundled (brotli) (time) 54.55 KB (0%)
react-on-rails-pro/client bundled (gzip) 64.92 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 64.92 KB (0%)
react-on-rails-pro/client bundled (brotli) 55.82 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 55.82 KB (0%)
registerServerComponent/client bundled (gzip) 135.42 KB (0%)
registerServerComponent/client bundled (gzip) (time) 135.42 KB (0%)
registerServerComponent/client bundled (brotli) 81.72 KB (0%)
registerServerComponent/client bundled (brotli) (time) 81.72 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 127.87 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 127.87 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 74.88 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 74.88 KB (0%)

Comment thread packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts Outdated

@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: 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 `@packages/react-on-rails-pro-node-renderer/tests/vmSourceMapSupport.test.ts`:
- Around line 312-345: Update both oversized external-map tests in
packages/react-on-rails-pro-node-renderer/tests/vmSourceMapSupport.test.ts
(anchor lines 312-345 and sibling lines 347-381) to resolve
fs.realpathSync(mapPath) once and use that resolved path in the
readFileSyncSpy/readFileAsyncSpy not-to-have-been-called assertions; preserve
the existing 'utf8' argument and all other test behavior.
🪄 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: 08004026-96cd-4e69-8178-984c46385f61

📥 Commits

Reviewing files that changed from the base of the PR and between 6dd4790 and 8ce909a.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts
  • packages/react-on-rails-pro-node-renderer/tests/vmSourceMapSupport.test.ts

AbanoubGhadban and others added 2 commits July 16, 2026 11:07
Review feedback from @greptile-apps (P1) and @chatgpt-codex-connector (P2) on #4688:
returning `undefined` for an oversized map made it indistinguishable from "no map
here", so `readSourceMapJsonForBundle`'s candidate loop fell through to the
conventional `<bundle>.js.map` fallback. When that fallback is a different or stale
map, frames were remapped to the WRONG sources rather than keeping their bundled
locations as the cap advertises — worse than not remapping at all.

Reproduced first: a bundle naming an oversized `explicit-oversized.js.map` with a
stale `<bundle>.js.map` alongside it remapped to the stale map's source.

`resolveSourceMapPathWithinSizeLimit` now returns a discriminated
usable/oversized/unusable result, and the readers surface `oversized` distinctly.
The candidate loops stop on it and return `null` ("found, permanently unusable")
instead of `undefined` ("nothing here, may arrive later"). `preloadSourceMapJsonForBundle`
maps that `null` to `retryMissingSourceMap: false`, which also closes the second half
of the codex finding: an oversized preload left the miss retryable, so a later
same-path map could remap an old VM generation and break generation isolation.

Also from review:
- Bound `warnedOversizedSourceMapPaths` as an insertion-ordered FIFO (256 entries).
  It previously grew one entry per distinct oversized map path for the process
  lifetime, which is ironic in a memory-retention PR (@claude). Evicting the oldest
  only risks a duplicate warning; clearing on unregister would re-warn on every VM
  rebuild and defeat the throttle.
- Assert on the realpath in the read-spy assertions, since the readers read the
  realpath and macOS resolves /var -> /private/var, so a regression could have
  slipped past (@coderabbitai).
- Fix "%d bytes exceeds" subject/verb agreement in the warning (@claude).

Two tests added, both verified to fail against the previous commit: the stale-fallback
repro and terminal-on-preload (an under-cap map appearing later must not remap that
generation). 47/47 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous commit made oversized maps terminal only at preload. A registration
that was lazy and retryable (map absent at build) still ran the retry budget when
the map later turned up oversized — so if that same path subsequently became
under-cap, this VM generation could remap with the newer file. That is the same
wrong-remap class the previous commit fixed for preload, and it made the CHANGELOG
claim ("will not retry the map for that VM generation") false for the lazy path.

`loadSourceMapForBundle` now returns a discriminated loaded/terminal/missing result
instead of `LoadedSourceMap | null`, which was collapsing the reader's `null`
(oversized, terminal) and `undefined` (nothing yet, retryable) back together right
after the previous commit went to the trouble of separating them.
`sourceMapForRegistration` caches the miss immediately on `terminal` and keeps the
existing retry budget for `missing`.

Deliberately still retryable: JSON.parse failures on a partially-written map, so a
completed map is still picked up (covered by the existing partial-content tests),
and createSourceMap returning null on old Node, to avoid an unrelated behavior change.

Found by Codex (xhigh) reviewing the previous commit. Regression test added and
verified to fail against it: missing at preload, oversized on first error, under-cap
later, must keep the bundled location. 48/48 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review (commit e2cf33a)

Re-reviewed the latest revision. This is a well-scoped, well-tested change (external .map files now share the pre-existing 50MB inline-map cap; zero extra syscalls by reusing the statSync result already fetched in resolveReadableSourceMapPath). I traced the terminal/retry state machine end-to-end (preloadSourceMapJsonForBundleregisterBundleForSourceMapsloadSourceMapForBundlesourceMapForRegistration) across both the async-preload and sync-lazy paths and didn't find a correctness bug — the boundary case (size === cap still remaps), the "terminal, never retried" case, and the "warn once per path" case all check out against their tests.

Two nits from earlier review rounds are now fixed:

  • warnedOversizedSourceMapPaths is now bounded (MAX_WARNED_OVERSIZED_SOURCE_MAP_PATHS = 256, FIFO eviction) instead of growing unboundedly for the process lifetime.
  • An oversized map found at preload now settles as retryMissingSourceMap: false immediately, instead of being folded into the generic "missing, retry up to 5 times" bucket.

One subtlety worth a second pair of eyes (not blocking): the "oversized" signal is now encoded two different ways depending on when it's discovered:

  • Sync/lazy path: readSourceMapJsonForBundle returns a literal null, which loadSourceMapForBundle maps straight to { status: 'terminal' }.
  • Preload path: registration.sourceMapJson is set to null up front, but loadSourceMapForBundle's ternary (vmSourceMapSupport.ts:676-679) takes the other branch for a non-undefined sourceMapJson, and null ?? undefined coerces it back to undefined — so it resolves as { status: 'missing' }, not 'terminal'. It still ends up permanently cached as null in sourceMapForRegistration, but only because that code path also checks !shouldRetryMissingSourceMap(registration), which happens to be true because retryMissingSourceMap: false was paired with it. The comment at vmSourceMapSupport.ts:681-683 documents this, and it's covered by the "terminal and not retried later" tests for both paths, so it's not a live bug — but the correctness now depends on an invariant (every sourceMapJson: null from preloadSourceMapJsonForBundle must be paired with retryMissingSourceMap: false) that isn't enforced by the type system. A future change to preloadSourceMapJsonForBundle that returns null with retryMissingSourceMap: true would silently reintroduce pointless re-stat churn without any test catching it (none of the new tests construct that combination). Might be worth a one-line assertion or a type that couples the two, but I wouldn't hold up the PR for it.

Still outstanding from a prior review round (minor, explicitly out of this PR's stated scope): parseDataUrlSourceMap still silently drops oversized inline (data:) maps with no log.warn, while this PR adds an explicit warning for external maps — a small observability asymmetry between the two cap sites.

No security or performance concerns beyond what's already documented in the PR description (the TOCTOU window between statSync and the read is honestly called out in both the code comment and CHANGELOG). Nice work.

Review nit from @claude on e2cf33a: `loadSourceMapForBundle`'s ternary coerced a
registration's own `sourceMapJson: null` ("confirmed no usable map") to `undefined`
via `?? undefined`, so it resolved as `missing` rather than `terminal`. It still
settled correctly, but only because `sourceMapForRegistration` separately checks
`shouldRetryMissingSourceMap`, which is only false because `preloadSourceMapJsonForBundle`
happens to always pair `sourceMapJson: null` with `retryMissingSourceMap: false`.
That invariant is not type-enforced, so the correctness was resting on a coincidence.

`null` on a registration now returns `{ status: 'terminal' }` explicitly, before the
ternary. Same behavior today, no longer dependent on the pairing holding.

Honest note on the accompanying test: it is a guard, not a regression reproducer. I
verified it passes against the previous commit too, because the `??` coercion already
prevented the read and the only difference was retry-counter churn, which is not
observable through this API. It pins the invariant the reviewer noted was untested
(`sourceMapJson: null` + `retryMissingSourceMap: true` must still never read the map)
so a future refactor cannot quietly break it. 49/49 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AbanoubGhadban

Copy link
Copy Markdown
Collaborator Author

@claude — on the last outstanding point, the inline (data:) map warning asymmetry: declining it for this PR, with reasoning.

I checked whether inline oversized maps have the same defect the external ones did, since that would have changed the answer. They do not. parseDataUrlSourceMap returning undefined for an oversized inline map is already terminal on every path:

  • readSourceMapJsonForBundle / ...Async early-return for data: URLs, so there is no candidate loop to fall through to — the stale-fallback bug that made the external case a P1 cannot occur here.
  • preloadSourceMapJsonForBundle uses parseDataUrlSourceMap(url) ?? null with retryMissingSourceMap: false.
  • registerBundleForSourceMaps likewise coerces to null.

So the gap is observability only — an oversized inline map is correctly ignored, just silently. Adding a warning there means either warning on every parseDataUrlSourceMap call (it runs from three call sites, including every VM build) or inventing a throttle key for something that has no path to key on, since the map is embedded in the bundle text. That is a real design question, not a one-liner, and it is squarely outside a cap-only PR on a parked/P3 issue.

Filing it as a follow-up rather than doing it here. Thanks for the thorough re-review — the state-machine trace across both paths is what surfaced the ?? coercion nit, which was worth fixing.

@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AbanoubGhadban's task in 1m 12s —— View job


Tasks

  • Read trigger comment and gather PR context
  • Verify the claim: parseDataUrlSourceMap oversized-inline-map path is already terminal on all call sites (no candidate-loop fallthrough, no retry)
  • Verify readSourceMapJsonForBundle/...Async early-return for data: URLs
  • Verify preloadSourceMapJsonForBundle and registerBundleForSourceMaps coercion to null
  • Post verification verdict

Verification

Traced all four inline-map paths against vmSourceMapSupport.ts on this commit (75d809f):

  • No candidate-loop fallthroughreadSourceMapJsonForBundle (line 447) and readSourceMapJsonForBundleAsync (line 480) both check sourceMappingUrl.startsWith('data:') and return parseDataUrlSourceMap(...) immediately, before the candidatePaths loop is ever entered. So there's no analog to the external-map bug (falling through to a stale <bundle>.js.map) — an inline map has no "next candidate" to fall through to. Confirmed correct.
  • No retry churnpreloadSourceMapJsonForBundle (lines 507–511) returns { retryMissingSourceMap: false, sourceMapJson: parseDataUrlSourceMap(sourceMappingUrl) ?? null } directly on the data: branch, skipping the async reader entirely. registerBundleForSourceMaps (lines 560–563) independently coerces the same way for its own inlineSourceMapJson computation. Both terminal, both immediate. Confirmed correct.

So the claim holds: an oversized inline map is already handled as a correct terminal null on every path — there is no correctness gap, only the observability one (no log.warn), same conclusion @AbanoubGhadban reached.

One small correction to the writeup, immaterial to the conclusion: parseDataUrlSourceMap has four call sites in this file (447, 480, 510, 562), not three — registerBundleForSourceMaps is a fourth. It doesn't change the argument; if anything it strengthens it, since it means a warning would need a throttle key shared across even more call sites for something that, as noted, has no natural key (no distinct path — the map is embedded in the bundle text itself).

Deferring this as a follow-up rather than folding it into a cap-only PR on a parked/P3 issue is the right scope call. Agreed — no further concerns on this thread.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75d809fc39

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/react-on-rails-pro-node-renderer/src/worker/vmSourceMapSupport.ts Outdated
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review

Overview
This PR extends the pre-existing 50MB inline (data:) source-map size cap to external .map files. It is scoped exactly as described in the PR body: resolveReadableSourceMapPath now returns { sourceMapPath, sizeInBytes } (reusing the existing statSync call - no new syscall), and a new resolveSourceMapPathWithinSizeLimit applies the cap on both the async preload path (buildVM) and the sync lazy path (error-time lookup). Oversized maps are treated as terminal (not retried, not falling through to a different candidate map) and produce a warning throttled to once per map path.

Code quality

  • The tri-state modeling (usable / oversized / unusable for path resolution, loaded / terminal / missing for map loading, and the string | null | undefined JSON result convention) cleanly distinguishes "no map yet, may retry" from "map found but permanently unusable" without overloading undefined/null ambiguously. Comments consistently explain why each state exists.
  • The terminal short-circuit in loadSourceMapForBundle (checking registration.sourceMapJson === null before doing any work) correctly avoids re-stating/re-warning on every subsequent lookup for the same registration/generation - good for the "warn once" requirement and avoids repeated wasted syscalls on a VM that already determined its map is unusable.
  • The generation-isolation design (new registration per VM build -> fresh terminal determination) is preserved; the PR explicitly validates this against the existing "same-path rebuild" test rather than breaking it, the right call given that test encodes a real guarantee.

Test coverage
The new "external source map size cap" describe block is thorough: preload-path oversized skip (with readFile/readFileSync asserted not called - verifying the cap actually avoids the read, not just the remap), sync lazy-path oversized skip, an exact-boundary test (size === cap still remaps, guarding the > vs >= choice), warn-once-across-repeated-lookups, terminal-not-retried on both preload and lazy paths, and explicit-vs-fallback candidate precedence. Good coverage for a change with several interacting states.

Security
Net security improvement - closes an unbounded-memory-read path for external maps (previously only inline maps were capped). Existing symlink-swap protections (realpath-then-stat inside realBundleDirectory) are preserved unchanged.

Minor observations (non-blocking, also left as inline comments)

  1. Unit mismatch between the two caps: MAX_EXTERNAL_SOURCE_MAP_BYTES compares actual file bytes (stats.size), while the pre-existing inline cap compares JS string length (payload.length/decoded.length), not byte count. Aliasing the new constant to the old one implies parity that does not quite hold for non-ASCII map content. Pre-existing issue, not introduced here, but worth a follow-up or a clarifying comment.
  2. warnedOversizedSourceMapPaths never resets a path once warned, even if the map later shrinks under the cap and then grows over it again at the same path. The design comment explains the FIFO bound well but not this one-shot-forever aspect. Given the stated per-deploy-path assumption this is likely fine in practice, but worth documenting explicitly.

Verdict
No correctness bugs found in the core logic - the terminal/retry/cache state machine holds up under the scenarios traced through (preload-oversized, lazy-oversized, boundary, same-path rebuild, confirmed-null registration). The PR is honest about its scope and explicitly documents what it does not fix (the "successful bundle still retains its map for VM lifetime" issue), the right call to leave as a separate product decision. Good to merge from a code-quality/correctness standpoint.

Review finding from @chatgpt-codex-connector (P2) on 75d809f, reproduced before
fixing. Making an oversized map terminal was too aggressive at the candidate-loop
level:

A bundle names `named.js.map`, which is missing at build (the supported
late-arriving-map flow), while an oversized `<bundle>.js.map` sits at the
conventional fallback path. The loop skipped the missing named map, hit the
oversized fallback, and returned the terminal `null` — so `preloadSourceMapJsonForBundle`
registered `sourceMapJson: null, retryMissingSourceMap: false` and the named map was
never picked up when it arrived. The frame kept its bundled location forever.

The two goals are distinct: an oversized candidate must stop *substitution* (never
remap through a map the bundle does not name — the original P1), but it must not
retire the *retry* for a higher-priority candidate that was merely missing and can
still show up. The loops now track whether a higher-priority candidate was missing
and return the retryable `undefined` in that case, keeping the terminal `null` for
when the oversized map is itself the best candidate.

Also documents two things @claude noted:
- `MAX_EXTERNAL_SOURCE_MAP_BYTES` and `MAX_INLINE_SOURCE_MAP_BYTES` share a number
  but not a unit (bytes on disk vs UTF-16 string length), so the alias implies a
  parity that does not quite hold. Pre-existing; noted rather than changed.
- A warned path is never re-armed, so a reused path whose map dips under the cap and
  grows back may not warn twice. The map is still skipped correctly; only the
  warning is suppressed.

Regression test added, verified to fail against 75d809f. The existing
oversized-terminal tests still pass, so this does not regress the original fix.
50/50 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review: Cap the size of external source maps read by the node renderer

Overview: Extends the existing 50MB inline-source-map cap to external .map files. resolveReadableSourceMapPath now reuses the statSync it already performs (no extra syscall), and a new resolveSourceMapPathWithinSizeLimit applies the cap on both the async preload path and the sync lazy path. Oversized maps are terminal (never fall through to a lower-priority candidate, never retried for that VM generation) and warn once per map path via a bounded (256-entry) FIFO set.

Correctness

I traced the tri-state logic (usable / oversized / unusable candidates, and loaded / terminal / missing load results) through all the branch combinations in readSourceMapJsonForBundle(Async), loadSourceMapForBundle, and sourceMapForRegistration, including:

  • explicit map oversized → terminal immediately, fallback never consulted (deliberate — matches the "don't remap through an unnamed map" rationale in the comments)
  • fallback map oversized but a higher-priority named map was merely missing → stays retryable (higherPriorityCandidateMissing flag), so a later-arriving named map can still win
  • boundary case (size === cap) still remaps (strict > comparison, consistent with the pre-existing inline-map check)
  • a preload-detected oversized map (registration.sourceMapJson === null) short-circuits loadSourceMapForBundle without touching disk again
  • a lazily-detected oversized map gets cached as null in sourceMapCache, so a later-shrunk map at the same path is correctly not picked up for that VM generation (intentional per the generation-isolation guarantee described in the PR)

All of this checks out — I didn't find a correctness bug in the branching logic, and it's consistent with the four scenarios called out in the test suite plus a few I traced manually (e.g., the FIFO warn-dedup eviction correctly evicts oldest-first via Set insertion order).

Security

Verified the cap is the only path that reads external maps — packages/react-on-rails/src/errorUtils.ts and serverRenderUtils.ts only call into remapStackTrace, which routes through this same module, so there's no parallel uncapped read path left. The realpath/allowlist containment checks are untouched by this diff.

Minor observations (non-blocking)

  • Test fragility: the test file re-declares MAX_EXTERNAL_SOURCE_MAP_BYTES = 50 * 1024 * 1024 locally rather than importing it (it's not exported), with a comment explaining why (avoids a 50MB+ disk write per test). This is a reasonable tradeoff, but it does mean the "exactly at the cap" boundary test would silently stop testing the real boundary if the production constant is ever changed without updating the test's mirrored value — worth a // keep in sync with MAX_EXTERNAL_SOURCE_MAP_BYTES style comment reminder, though one already exists.
  • The unit mismatch between MAX_INLINE_SOURCE_MAP_BYTES (UTF-16 code units) and the new MAX_EXTERNAL_SOURCE_MAP_BYTES (disk bytes) sharing the same numeric constant is called out clearly in a comment — good that it's documented rather than silently inconsistent, and out of scope to fix here per the PR's stated narrow scope.
  • The TOCTOU gap (file can grow between statSync and the actual read) is explicitly disclosed in both the code comment and PR description as an "honest limitation" rather than oversold — appropriate given this is a pre-read gate, not a hard memory bound.

Test coverage

The 9 new tests are well-targeted: async preload path, sync lazy path, boundary equality, candidate-priority interactions (oversized named vs. stale fallback, oversized fallback vs. late-arriving named), terminal/no-retry behavior on both preload and lazy discovery, and warn-dedup. The "vacuity check" described in the PR body (tests fail when only the src change is reverted) is good practice.

Overall: a tightly-scoped, well-tested fix with careful attention to the existing generation-isolation invariants. No blocking issues found.

@AbanoubGhadban

Copy link
Copy Markdown
Collaborator Author

@coderabbitai — you were right about the CHANGELOG, fixed in 8c2910f.

The entry claimed an oversized map "will not retry the map for that VM generation" unconditionally, which stopped being true in fd9c196cb — that commit deliberately keeps the retry alive when the oversized map is a lower-priority fallback behind a still-missing named map. My commit message drew that distinction and then I failed to carry it into the changelog. It now reads:

An oversized map is never substituted: the renderer will not fall back to a differently-named map alongside the bundle. When the oversized map is the one the bundle names, that result is final for the VM generation and is not retried; a map that merely arrives late is still retried as before.

This is the second time a reviewer caught this exact class of drift on this PR (@chatgpt-codex-connector caught the same overstatement about the lazy path earlier), which is a fair signal about my changelog discipline.

On the unusable-vs-missing loose end: agreed, and agreed it is not a bug. A permanently-invalid candidate (symlink escape, not-a-file) keeps a lower-priority oversized map retryable rather than terminal, costing a few bounded retries before MAX_MISSING_SOURCE_MAP_RETRIES retires it. Distinguishing "invalid forever" from "not there yet" would mean a fourth state through the whole reader contract, for a saving of at most four stats on a misconfigured bundle. Not worth it here — leaving it.

Thanks for the exhaustive enumeration; it matches the one I ran independently, which is a useful cross-check on a state machine that has now had two real bugs in it.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
git log --oneline -5 --all | grep -i "8c2910f" || true
git show 8c2910f7 -- CHANGELOG.md || true

Length of output: 2647


@AbanoubGhadban Confirmed — commit 8c2910f7 updates the CHANGELOG exactly as described: it now correctly distinguishes "never substituted" (always true) from "final for the VM generation" (only true when the oversized map is the named one), with late-arriving maps still retried. That matches the actual fd9c196c behavior.

Agreed on leaving the unusable-vs-missing distinction alone — as noted, it's bounded by MAX_MISSING_SOURCE_MAP_RETRIES and not worth a fourth reader-contract state for a handful of stat calls on a misconfigured bundle.

Nothing else outstanding from my side on this thread.

@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review

Solid, well-scoped PR — the implementation matches its own extensive design notes and the test suite exercises every corner case called out in the description (terminal-vs-retryable, generation isolation, priority ordering, boundary at exactly the cap, warn-once).

Code quality

  • The unusable / oversized / usable tri-state (ResolvedSourceMapCandidate) and the loaded / terminal / missing state (SourceMapLoadResult) are a clean way to thread "confirmed-too-big" through the existing retry machinery without conflating it with a plain cache miss. The comments explaining why terminal must not fall through to a lower-priority candidate (readSourceMapJsonForBundle) are genuinely useful given how non-obvious that invariant is.
  • Reusing the statSync that resolveReadableSourceMapPath already performs (returning { sourceMapPath, sizeInBytes }) avoids a redundant syscall — nice attention to the existing hot path.
  • The warnOversizedSourceMap FIFO-bounded Set for "warn once per path" is a reasonable, low-complexity throttle for a value that's re-checked on every VM build/error lookup.

Minor observations (non-blocking)

  • Unit mismatch, called out but worth double-checking in practice: MAX_INLINE_SOURCE_MAP_BYTES is compared against a JS string length (UTF-16 code units) while MAX_EXTERNAL_SOURCE_MAP_BYTES (same numeric value) is compared against on-disk byte size. The comment is transparent about this, but it does mean the two "50MB" limits admit meaningfully different amounts of actual JSON depending on multi-byte content in sources/sourceContent. Not a bug, just worth flagging so it doesn't get read as a stricter guarantee than it is.
  • TOCTOU window is explicitly accepted: a map that grows between statSync and the actual readFileSync/readFile is still read in full. This is disclosed in the code comment, PR description, and CHANGELOG, so it's a documented tradeoff rather than an oversight — but since this is the entire point of the cap, it's worth the maintainers explicitly deciding whether that residual risk is acceptable long-term (e.g., for an attacker who can influence map size and time a rebuild).
  • Sync/async duplication: readSourceMapFile/readSourceMapFileAsync and readSourceMapJsonForBundle/readSourceMapJsonForBundleAsync are near-identical control flow duplicated twice. Pre-existing pattern in this file, not introduced by this PR, so not a blocker — just flagging as an efficiency/maintenance nit if a future refactor wants to unify them behind a shared candidate-iteration helper parameterized by a read function.

Correctness spot-check

Traced through the priority-fallback logic by hand for the tricky cases:

  • First (highest-priority) candidate oversized → terminal null immediately, no fallthrough to a differently-named map (verified against resolveSourceMapPathWithinSizeLimit/readSourceMapJsonForBundle).
  • Higher-priority candidate merely missing, lower-priority candidate oversized → stays retryable (undefined), so a named map that hasn't arrived yet isn't permanently blocked by an oversized <bundle>.js.map sitting at the fallback path.
  • Exactly-at-cap uses > (not >=), so the boundary is inclusive of the cap, matching the pre-existing inline-map behavior.
  • Terminal caching goes through the same sourceMapCache WeakMap as a normal miss, scoped to the registration (VM generation), so a same-path rebuild gets a fresh registration and isn't stuck with a stale terminal answer forever — confirmed by tracing registerBundleForSourceMaps → new registration object per rebuild.

No functional bugs found. Test coverage is thorough and matches the documented scope (async preload path, sync lazy path, boundary, warn-once, terminal-not-retried, priority-fallback interactions). CHANGELOG entry follows existing formatting/conventions.

@AbanoubGhadban

Copy link
Copy Markdown
Collaborator Author

Review round complete — all threads addressed and resolved

Two of the findings were real bugs I had introduced. Both were reproduced with a failing test before any fix, and both regression tests were verified to fail against the commit that shipped the bug.

Must-fix (both confirmed by repro, not taken on faith)

1. Oversized map fell through to a stale fallback — P1, @greptile-apps + @chatgpt-codex-connector (06779216e)
Returning undefined for an oversized map made it indistinguishable from "no map here", so readSourceMapJsonForBundle's candidate loop continued to the conventional <bundle>.js.map. Frames were remapped through a map the bundle never named — worse than not remapping, which is precisely what the cap advertises. Repro output before the fix:

at boom (webpack:/test-app/components/RebuiltBoom.ts:2:3)   ← the stale fallback's source

Fixed with a discriminated usable/oversized/unusable result; the loops now stop on oversized.

2. My fix for #1 was too aggressive — P2, @chatgpt-codex-connector (fd9c196cb)
Making oversized terminal at the loop level broke the supported late-arriving-map flow: a bundle naming a still-missing named.js.map, with an oversized <bundle>.js.map at the fallback path, went terminal and never picked up the named map when it arrived. The two goals were conflated — an oversized candidate must stop substitution, but must not retire the retry for a higher-priority candidate that is merely missing. Now discriminated by higherPriorityCandidateMissing.

3. Lazy path + registration state@claude (e2cf33a22, 75d809fc3)
Oversized is now terminal on the lazy path too (it previously left the CHANGELOG claim false), and a registration's confirmed-null surfaces terminal directly instead of resting on an un-enforced invariant.

Also fixed

  • Bounded warnedOversizedSourceMapPaths (FIFO, 256) — @claude
  • Realpath in read-spy assertions — @coderabbitai
  • Log grammar — @claude
  • CHANGELOG overstated the retry behavior after fd9c196cb@coderabbitai (8c2910f7e). Second changelog-drift catch on this PR; fair signal.

Declined, with reasons

  • Inline (data:) map warning asymmetry — verified inline oversized is already terminal on all three paths (early return, no candidate loop), so this is observability-only. Adding a warning needs a throttle key that embedded maps don't have. Follow-up, not a cap-only PR.
  • unusable vs missing distinction — costs at most 4 bounded stats on a misconfigured bundle; a fourth reader state isn't worth it.
  • Unit mismatch (stats.size bytes vs UTF-16 length) and sync/async duplication — both pre-existing; the former is now documented rather than silently implied away.

One correction against my own favor

I added a test for the sourceMapJson: null + retryMissingSourceMap: true invariant, then found it passes against the previous commit too — the ?? coercion already prevented the read, and the only difference was unobservable retry churn. It's a guard, not a regression reproducer, and I'd rather say so than let it read as proof. @claude's "not a live bug" call was right.

Status

  • 50/50 tests green (9 new), CI green on 8c2910f7e (11 success / 0 failure / 19 skipped)
  • All 10 review threads replied to and resolved
  • Re-review of the final logic requested and returned clean: @chatgpt-codex-connector "didn't find any major issues", @coderabbitai independently enumerated every candidate ordering and confirmed, @claude "no functional bugs found"

Scope unchanged: this remains the narrow cap @justin808 sanctioned. It still does not fix the issue's headline complaint (zero-error bundles retaining their map), which can't be fixed without breaking the generation-isolation guarantee test :895 encodes — a product decision, written up on #4313.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@AbanoubGhadban Thanks for the thorough wrap-up — nice trace-driven fix on the candidate state machine, and appreciated the transparency on both the bugs and the self-correction on the invariant test. Nothing further from my side; scope, declines, and rationale all look sound for a narrow cap fix. Good to merge as far as the review threads I raised are concerned.

@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Claude finished @AbanoubGhadban's task in 2m 6s —— View job


Final verification complete

  • Read the summary comment and full review thread history
  • Verify the current diff matches the claims
  • Spot-check test coverage
  • Post final verdict

I re-read vmSourceMapSupport.ts directly (not just the diff) to check the summary's claims against the code as it stands on 8c2910f, rather than taking the write-up at face value:

  • Discriminated states are real, not just described. ResolvedSourceMapCandidate (usable/oversized/unusable, lines 381–384), the OVERSIZED_SOURCE_MAP symbol distinguishing "too big" from "not found" (line 405), and SourceMapLoadResult (loaded/terminal/missing, lines 678–681) are all present and threaded through consistently.
  • higherPriorityCandidateMissing (lines 466, 500) is implemented identically in both the sync and async candidate loops, and does exactly what the write-up says: an oversized candidate is terminal (null) only when nothing higher-priority is still missing; otherwise it stays retryable (undefined) so a late-arriving named map can still win.
  • Lazy-path terminal state: loadSourceMapForBundle checks registration.sourceMapJson === null up front (line 697) and returns { status: 'terminal' } directly — it no longer depends on the ?? undefined coercion + retryMissingSourceMap: false pairing holding by convention.
  • warnedOversizedSourceMapPaths is a bounded FIFO (MAX_WARNED_OVERSIZED_SOURCE_MAP_PATHS = 256, lines 130–145), evicting oldest-first, with the rationale (why not tie cleanup to unregisterBundleForSourceMaps) recorded in a comment.
  • CHANGELOG.md (lines 40–51) matches the corrected wording verbatim: "never substituted" stated unconditionally, "final for the VM generation" scoped correctly to the case where the oversized map is the one the bundle names, late-arriving maps still retried.
  • Test coverage: confirmed all the named tests exist in tests/vmSourceMapSupport.test.ts — the fallback-fallthrough repro, preload/lazy oversized-terminal-not-retried pairs, the late-arriving-named-map-not-terminal case, and the warn-once-per-path test.

I wasn't able to execute the Jest suite myself in this session (the sandbox declined the npx jest invocation), so I can't independently re-confirm the 50/50 pass count — but the code inspection above supports the summary's description of the current state, and CI is reported green on 8c2910f.

Assessment: the state machine holds together under inspection — the fixes for the two real bugs (stale-fallback fallthrough, and the fix-for-the-fix that restored late-arriving-map retries) are both present and match their descriptions, and the two open "declined" items (inline-map warning asymmetry, unusable vs missing granularity) are reasonable scope calls for a cap-only PR. No new issues found. Good to merge from my side, pending final maintainer sign-off.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown

This is a clean re-review pass. The final state of the code is solid — the reported bugs are genuinely fixed and the logic is correct throughout.

What I verified in the diff:

The higherPriorityCandidateMissing flag correctly handles the two competing concerns. When the named map is undefined (missing) and the fallback is OVERSIZED_SOURCE_MAP, the loop returns undefined (retryable), preserving the late-arrival window. When the named map itself is oversized, it returns null (terminal) immediately. The two-test pair covering this — "oversized explicit map does not fall through to the conventional fallback map" and "an oversized fallback map does not make a late-arriving named map terminal" — forms a precise regression lock on both directions.

The registration.sourceMapJson === null early-return in loadSourceMapForBundle is exactly right. The old ?? undefined coercion was the silent gap that let shouldRetryMissingSourceMap become the de facto gatekeeper instead of the explicit intent. The guard test ("a registration with a confirmed no-map result never reads, even if marked retryable") pins that invariant independently of the preload pairing it with retryMissingSourceMap: false.

The FIFO eviction on warnedOversizedSourceMapPaths (delete-oldest-on-overflow) is correct and bounded. resetSourceMapSupport clearing it is necessary for test isolation and you've got it.

The createSourceMap null result still returns { status: 'missing' } — correct, since a parse failure on a partially-written map should remain retryable so the completed write can be picked up.

One minor note (non-blocking): the warnSpy assertion in the async preload test checks expect.stringContaining(path.basename(mapPath)) for the second argument, but log.warn is called with positional format args (%s, %d, %d). If the logger doesn't format before calling the underlying function, the second argument passed to the spy is sourceMapPath (the full realpath), not the formatted string. On most platforms path.basename(fullRealpath) === path.basename(mapPath) so this passes, but it's testing the spy's second positional arg against a substring of the basename — worth a comment if this ever confuses a future reader. Not a correctness issue.

50/50 green, all threads resolved, scope maintained. This looks ready for merge pending maintainer sign-off on the product scope decision.

No behavior change; 50/50 tests unchanged, including both bug-repro regression tests
and the generation-isolation test.

The cap landed as its own layer on top of path resolution, which left four encodings
of the same tri-state chained through six levels. `resolveReadableSourceMapPath` had
exactly one caller and returned `{sourceMapPath, sizeInBytes}` purely to hand the size
to that caller — a value with no other consumer.

Folding the cap into `resolveReadableSourceMapPath` (which already ran the `statSync`
for its own `isFile` check) deletes `resolveSourceMapPathWithinSizeLimit`, the
`ResolvedSourceMapCandidate` union, and that object shape. `SourceMapFileReadResult`
becomes `SourceMapCandidateResult`, shared by resolve and read since their state spaces
are identical. Net -26 lines.

The containment order is unchanged and still ends with the read: lexical containment ->
realpathSync -> realpath containment -> statSync/isFile -> size check -> read. The size
check only ever runs on a realpath already proven inside the real bundle directory, so
the symlink-swap protection and its codeql justification are untouched. Still one
`statSync` per candidate.

Also drops a provably dead clause in `sourceMapForRegistration`:
`(sourceMappingUrl === null && !retryMissingSourceMap)` implies `!shouldRetryMissingSourceMap`,
so it could never be the deciding condition. It predates this PR, but this PR already
rewrote that branch.

Corrects the warn-throttle comment. It claimed the sync path floods "per error", which is
false — terminal caching means that path is never re-entered. The real driver is that
preload re-checks on every VM build, so an app with more bundles than `maxVMPoolSize`
(default 2) rebuilds per request and would warn at request rate.

Kept deliberately, per review: the OVERSIZED_SOURCE_MAP symbol (it means "this candidate
is oversized", which is NOT the higher level's "resolution is terminal" — `[missing,
oversized]` returns the symbol at one level and a retryable undefined at the next;
collapsing them would recreate the conflation behind the original P1), and
`SourceMapLoadResult` / `higherPriorityCandidateMissing`, which are the minimal state
for those two fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review: Cap the size of external source maps read by the node renderer

Overview: this narrowly extends the existing 50MB inline-source-map cap to external .map files. It reuses the statSync already performed in resolveReadableSourceMapPath (no extra syscall), makes an oversized-map result terminal for the VM generation (via a new OVERSIZED_SOURCE_MAP sentinel and a 'loaded' | 'terminal' | 'missing' tri-state in loadSourceMapForBundle/SourceMapLoadResult), and throttles the warning to once per map path with a bounded FIFO Set. Scope is explicitly limited per the PR description (doesn't touch the separate "pooled VM retains map bytes forever" problem from #4313).

Code quality

  • The tri-state result type (loaded/terminal/missing) is a clean way to distinguish "confirmed unusable" from "not found yet, may still arrive," and it correctly threads through preloadSourceMapJsonForBundle, registerBundleForSourceMaps, and loadSourceMapForBundle so a same-path rebuild still gets a fresh, non-terminal registration.
  • The higherPriorityCandidateMissing logic (don't fall through to a lower-priority candidate on oversized, but don't go terminal if a higher-priority candidate simply hasn't arrived yet) is subtle but well-commented and covered by dedicated tests (oversized explicit map does not fall through..., an oversized fallback map does not make a late-arriving named map terminal).
  • The size check is correctly placed after the realpath/containment checks in resolveReadableSourceMapPath, so it never runs on an unvalidated path.
  • Comments do a good job explaining why (unit mismatch between the inline cap comparing UTF-16 string length vs. the external cap comparing stats.size in bytes; FIFO eviction semantics for the warn-once Set; why a stat-then-read gate isn't a hard memory bound).

Minor observation (not blocking)

  • There's a latent asymmetry between how an oversized inline (data:) map is handled depending on code path. At registration time (registerBundleForSourceMaps) and in preloadSourceMapJsonForBundle, an oversized inline map correctly becomes terminal (null, via parseDataUrlSourceMap(...) ?? null). But readSourceMapJsonForBundle/readSourceMapJsonForBundleAsync's own data: branch just returns parseDataUrlSourceMap(sourceMappingUrl) (undefined when oversized), which loadSourceMapForBundle would treat as 'missing' (retryable) rather than 'terminal'. Today this is unreachable in production because the single call site (vm.ts:257) always supplies bundleContents, so registerBundleForSourceMaps always resolves inline maps at registration time before this branch is ever reached lazily. Worth a one-line comment (or normalizing to ?? null there too) so it doesn't become a live bug if this function gains another caller.

Tests

Coverage is thorough — preload vs. lazy-sync paths, exact-boundary (size === cap), terminal-not-retried, warn-once-per-path, and the priority/fallback interaction are all exercised. The vacuity-check note in the PR description (tests fail with only the src change reverted) is a good practice.

Security

No concerns — the size check runs only on an already realpath-validated, contained file, and the warning only logs the path/size server-side.

Performance

As claimed: the cap reuses the existing statSync call, adds no new syscalls, and the warn-once Set is bounded to 256 entries.

Overall: solid, well-scoped, well-tested change. No blocking issues found.

…ottle

The "warns once per map path" test was verified to be vacuous: with the dedup guard
neutralized it still passed. It called runInVM twice, but the first oversized detection
caches the terminal answer, so the second error never re-checks — exactly one warning
happened with or without dedup. It asserted nothing the preload test did not already
assert, and the throttle itself had no coverage.

Rewritten to exercise the repeat that actually matters: preload re-checks the map on
every buildVM, so the test builds, evicts via a second bundle at maxVMPoolSize 1, and
rebuilds. With the dedup removed it now fails (expected 1 warning, received 2) — the
check the old version passed.

Deleted `a registration with a confirmed no-map result never reads, even if marked
retryable`. It constructed a `sourceMapJson: null` + `retryMissingSourceMap: true`
registration that preload never emits, and the realistic version of that regression is
already covered by the preload-terminal test. It was also vacuous against the commit it
shipped in, which was reported on the PR.

Decoupled the warning assertions from the message wording. They pinned
`stringContaining('exceeds the')`, and this PR reworded that message — it survived by
luck. A warning naming the map is the requirement; its prose is not. They now assert the
data the warning carries (map path, size, limit) with `expect.any(String)` for the format.
The boundary test's negative assertion is matched by the map it names instead, since a
bare `expect.any(String)` there would have matched any 4-argument warning and weakened it.

Break-it verified, each reverted after: dedup removed -> the rewritten throttle test fails;
oversized-returns-undefined (the original P1) -> the fallthrough test fails with
RebuiltBoom.ts, the stale map's source; higherPriorityCandidateMissing never set -> the
late-arriving-map test fails. 49/49 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review: Cap the size of external source maps read by the node renderer

Scope check: This matches the PR description exactly — a narrow, cap-only change to vmSourceMapSupport.ts plus tests and a CHANGELOG entry. It does not attempt the steady-state VM-retention fix from #4313, which is explicitly (and correctly) called out as a separate product decision.

Code quality

  • The tri-state modeling (loaded / terminal / missing) replacing the old LoadedSourceMap | null return is a real improvement: it makes "found but permanently unusable" (oversized) explicit instead of overloading null/undefined, and every call site (loadSourceMapForBundle, sourceMapForRegistration, preloadSourceMapJsonForBundle) is updated consistently.
  • The size check reuses the statSync that isFile() already required — no added syscall, as claimed.
  • Comments are doing real work here (explaining why, e.g. the FIFO-vs-lifetime tradeoff for the warn-dedup set, the byte-vs-UTF-16-unit mismatch between the inline and external caps), not restating the code.
  • The OVERSIZED_SOURCE_MAP symbol sentinel plus higherPriorityCandidateMissing bookkeeping in readSourceMapJsonForBundle/readSourceMapJsonForBundleAsync correctly implements the stated policy: an oversized named map is terminal (no fallthrough to a different file), but an oversized map found only as a fallback while the named map is still missing stays retryable. I traced all four branches (named-oversized, fallback-oversized-with-named-missing, exactly-at-cap, warn-dedup) against the code and they check out.

Bugs

I didn't find a correctness bug. Specifically checked and found consistent:

  • Boundary is > not >=, matching the "exactly at cap still remaps" test.
  • Once a registration's sourceMapJson is frozen to null (oversized at preload) or the lazy-path cache records terminal, later same-path map changes cannot resurrect that VM generation — matches the "not retried later" tests and is consistent with the existing same-path-rebuild isolation guarantee the rest of the module relies on.
  • warnedOversizedSourceMapPaths FIFO eviction uses Set insertion order correctly, and is cleared by resetSourceMapSupport() (called from resetVM(), called from the test helper's beforeEach), so tests in the same describe block that reuse the same bundle path (via the shared BUNDLE_TIMESTAMP fixture) aren't polluted by leftover warn state across tests.

Security

This is a net security improvement (bounds a previously-unbounded memory read from external .map files) and doesn't weaken the existing path-containment checks — the size check runs after both realpath containment checks in resolveReadableSourceMapPath, so it can't be used to leak size information about files outside the bundle directory. The TOCTOU gap (map grows between statSync and the read) is honestly disclosed in comments/CHANGELOG rather than oversold, which I'd rather see than a false guarantee.

Performance

Negligible overhead: one integer comparison per candidate resolution, and the warn-dedup Set is capped at 256 entries.

Test coverage

Good — the four scenarios named in the PR description are all present, plus two extra (fallback-oversized-with-named-map-arriving-late, and warn-dedup-across-rebuilds) that specifically probe the trickier terminal/retryable interaction. I did not execute the suite in this environment (sandboxed), so I'm relying on the PR's stated 45/45 pass + vacuity-check results rather than reproducing them.

Minor suggestion (non-blocking)

Left as an inline comment: MAX_EXTERNAL_SOURCE_MAP_BYTES is duplicated as a literal in the test file (with a comment noting it's "not exported"). Consider exporting it as @internal Used in tests (same pattern already used for resetSourceMapSupport) so the test and the cap can't silently drift apart if the limit changes later.

…e-cap

Resolve CHANGELOG.md conflict by keeping both Unreleased/Fixed entries
(#4688 external source-map cap and #4690 RSC perf fallback bound).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review: Cap the size of external source maps read by the node renderer

Read through vmSourceMapSupport.ts in full (not just the diff) plus the new test block. Overall this is a careful, well-scoped change — the tri-state (loaded / terminal / missing) result modeling and the higherPriorityCandidateMissing bookkeeping correctly distinguish "oversized and therefore terminal" from "still might arrive" across the two-candidate (explicit sourceMappingURL vs. conventional <bundle>.js.map) lookup, and I traced each of the new tests against the logic and they match.

Correctness

  • The size > MAX_EXTERNAL_SOURCE_MAP_BYTES boundary is strict-greater, so exactly-at-cap still remaps — matches the "exactly at the cap" boundary test.
  • resolveReadableSourceMapPath reuses the existing statSync call (previously only used for isFile()), so the cap genuinely costs no extra syscall as claimed.
  • The terminal/oversized result is frozen into the registration (sourceMapJson: null) so a same-VM-generation retry can't pick up a later, smaller map at the same path — this is intentionally tested and matches the file's existing "same-path rebuild" isolation guarantee.
  • Warn-once-per-path via a bounded (256-entry) FIFO Set is a reasonable throttle for the async preload path, which reruns on every VM build.

Minor / non-blocking observations

  • PR description drift: the description says resolveReadableSourceMapPath now returns { sourceMapPath, sizeInBytes } and that a new resolveSourceMapPathWithinSizeLimit applies the cap. The actual diff instead returns a string | typeof OVERSIZED_SOURCE_MAP | undefined sentinel directly from resolveReadableSourceMapPath, with no separate resolveSourceMapPathWithinSizeLimit function. Not a code problem — the implementation is arguably simpler than what's described — but worth updating the PR description so it doesn't mislead reviewers/future readers about the actual shape of the change.
  • TOCTOU is explicitly disclosed: a map that grows between statSync and the read is still read in full. This is called out in both the code comment and CHANGELOG, so it's a known, accepted limitation rather than an oversight — flagging only so it's clear I didn't miss it, not asking for a fix.
  • Warn timing on the fallback path: when the higher-priority (explicit) candidate is merely missing and the lower-priority fallback candidate is oversized, the code still calls warnOversizedSourceMap for the fallback even though the final result is "retryable miss," not terminal (see an oversized fallback map does not make a late-arriving named map terminal test). That's arguably correct (the file really is oversized), but it means a legitimately large stale fallback file sitting next to a bundle that has its own small named map will emit one warning even though it's never actually used. Low severity, likely intentional given the comment on the throttle constant.
  • Could not execute pnpm jest tests/vmSourceMapSupport.test.ts or tsc --noEmit in this review sandbox (command approval unavailable), so I verified the new tests by static trace against the implementation rather than by running them. The PR description's own vacuity-check methodology (revert src, confirm tests 1/2/4 fail) is a good practice worth preserving if this is rebased.

Security

  • No new attack surface: the size check happens after the existing realpath/containment checks in resolveReadableSourceMapPath, so it doesn't change what's reachable — only whether an already-allowed file gets read.
  • The codeql[js/path-injection] suppressions on the two readFileSync/readFile calls are unchanged in placement and still sit directly above reads of an already-realpath-validated, contained path.

Test coverage

The 8 new tests in the external source map size cap describe block cover the cases that matter: async preload vs. sync lazy path, explicit-vs-fallback priority interaction in both directions, the exact-boundary case, terminal-not-retried for both discovery paths, and warn-once-per-path across a VM rebuild/eviction. I don't see a gap here.

No blocking issues found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b6b88663f1

ℹ️ 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".

Comment thread CHANGELOG.md
@justin808

Copy link
Copy Markdown
Member

Merge readiness (batch ror-c-20260716-1543-pro-node-residuals)

Confidence note: merging under batch merge_authority auto_merge_when_gates_pass (release mode: development per tracker #3823; target main → beta phase gate).

Evidence at head b6b88663f:

  • pr-ci-readiness: READY — required-pr-gate SUCCESS, 24 pass / 19 skipped / 0 fail / 0 pending
  • mergeStateStatus: CLEAN (CHANGELOG-only conflict with main resolved via non-force merge commit, both entries kept)
  • Review threads: 0 unresolved on current head — 2 claude advisory nits + 1 codex P2 wording nit dispositioned in-thread (all deferred to the imminent [Node Renderer] External source maps: no size cap on .map reads and double retention (raw JSON + parsed map) per pooled VM #4313 retention follow-up PR from this batch, which touches the same file/CHANGELOG section); no pushes for nits per review-loop convergence
  • script/pr-merge-ledger 4688 --strict: complete_allowed true, violations [], unknown_fields [] (changelog_present; author Must-fix wrap-up items verified fixed-in-PR via commit ancestry 0677921…8c2910f7e)
  • CodeRabbit APPROVED (superseding its earlier changes-requested); latest Codex/Claude/Greptile passes advisory-only

Follow-up commitments carried into the next batch PR: qualify the changelog memory-cap wording (pre-read gate), normalize the data:-branch tri-state, export the byte-cap constant for tests.

[lane ror-c-4313-koa]

@justin808

Copy link
Copy Markdown
Member

Completed-batch audit: replay evidence follows.

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