Skip to content

Add CacheSection component and section caching infrastructure (experimental)#4740

Open
AbanoubGhadban wants to merge 2 commits into
mainfrom
4385-selective-hydration-investigation
Open

Add CacheSection component and section caching infrastructure (experimental)#4740
AbanoubGhadban wants to merge 2 commits into
mainfrom
4385-selective-hydration-investigation

Conversation

@AbanoubGhadban

@AbanoubGhadban AbanoubGhadban commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Experimental implementation for investigating selective hydration caching in the dummy app:

  • CacheSection React component: Wraps children in Suspense with configurable delay for time-based section division during streaming
  • selective_hydration_cached controller action: Uses ActionController::Live to stream pre-cached HTML sections with configurable delays
  • section_cache rake task: Captures streamed HTML into section files by time windows (pure timing-based division, no HTML parsing)
  • SelectiveHydrationDemo: Updated to use CacheSection for 4 sections (header, article, categories, footer)
  • Dynamic CSP nonce replacement: Replaces cached nonces with current request's nonce when streaming

All changes are in react_on_rails_pro/spec/dummy (experimental).

How it works

  1. Generate cached sections:

    rake section_cache:generate[/selective_hydration_demo,4,5]

    This fetches the page with section delays [0, 5000, 10000, 15000]ms, capturing HTML chunks into time-based buckets.

  2. Serve cached sections with progressive streaming:

    curl http://localhost:5150/selective_hydration_cached?delay=1

    Streams each section with configurable delays, replacing CSP nonces dynamically.

  3. Progressive rendering in browser:

    • Section 0 (shell + header) arrives immediately
    • Section 1 (article) arrives after delay
    • Section 2 (categories) arrives after delay
    • Section 3 (footer) arrives after delay
    • React hydrates each section as it arrives via $RC calls

Test plan

  • Verify rake section_cache:generate creates section files
  • Verify /selective_hydration_cached streams sections progressively
  • Verify all sections render and hydrate without errors
  • Verify interactive elements work (buttons, inputs)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a selective hydration demo with independently loading page sections and configurable delays.
    • Added progressive streaming and cached-content demo pages.
    • Added interactive header search and navigation menu.
    • Added article interactions, including bookmarks, likes, and comments.
    • Added category selection and newsletter subscription controls.
    • Added footer feedback and contact modal interactions.
    • Added tools for generating, validating, and clearing cached demo content.

AbanoubGhadban and others added 2 commits July 15, 2026 17:37
…#4385)

Demonstrates selective hydration architecture with:
- Root component (SelectiveHydrationDemo.jsx) without "use client" directive
- Four child client components with "use client" that hydrate independently:
  - SelectiveHydrationHeaderSection (menu toggle, search)
  - SelectiveHydrationArticleSection (like button, comments)
  - SelectiveHydrationCategoriesSection (category selection, newsletter)
  - SelectiveHydrationFooterSection (feedback form, contact modal)

Each section is ~70vh tall to demonstrate below-the-fold content.
Child components are in components/SelectiveHydration/ (not auto-loaded).
Only the root component is in ror-auto-load-components/.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…app)

Experimental implementation for investigating selective hydration caching:

- Add CacheSection React component that wraps children in Suspense with
  configurable delay for time-based section division during streaming
- Add selective_hydration_cached controller action using ActionController::Live
  to stream pre-cached HTML sections with configurable delays
- Add section_cache rake task to capture streamed HTML into section files
  by time windows (no HTML parsing, pure timing-based division)
- Update SelectiveHydrationDemo to use CacheSection for 4 sections
- Replace CSP nonces dynamically when streaming cached content

All changes are in react_on_rails_pro/spec/dummy (experimental).

Usage:
  rake section_cache:generate[/selective_hydration_demo,4,5]
  curl http://localhost:5150/selective_hydration_cached?delay=1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a selective-hydration demo with four independently delayed React sections, Rails Live streaming endpoints, generated section-cache tasks, cached HTML replay, and server-bundle registration updates.

Changes

Selective hydration streaming

Layer / File(s) Summary
React sections and server composition
react_on_rails_pro/spec/dummy/client/app/components/..., react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/SelectiveHydrationDemo.jsx, react_on_rails_pro/spec/dummy/client/app/packs/server-bundle.js
Adds four interactive client sections, delayed CacheSection rendering with fallbacks, the server-side composition component, and generated server-bundle wiring.
Rails routes and streaming actions
react_on_rails_pro/spec/dummy/config/routes.rb, react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb, react_on_rails_pro/spec/dummy/app/views/pages/selective_hydration_demo.html.erb
Adds named demo routes, Live streaming controller actions, cached-section nonce replacement, streaming headers, and the page template that passes parsed section delays.
Cached section generation and replay
react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake
Adds tasks to capture streamed sections, verify expected HTML markers, and clean cached output.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant RailsRoutes
  participant PagesController
  participant ReactOnRails
  participant SelectiveHydrationDemo
  participant CacheSection

  Browser->>RailsRoutes: GET selective_hydration_demo
  RailsRoutes->>PagesController: dispatch action
  PagesController->>ReactOnRails: render streaming template
  ReactOnRails->>SelectiveHydrationDemo: pass sectionDelays
  SelectiveHydrationDemo->>CacheSection: render delayed sections
  CacheSection-->>Browser: stream section HTML and hydration placeholders
Loading

Suggested labels: ready-for-hosted-ci

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding CacheSection and experimental section caching infrastructure.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 4385-selective-hydration-investigation

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 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds an experimental selective-hydration section cache to the dummy app. The main changes are:

  • Delayed Suspense boundaries for four interactive page sections.
  • A rake task that captures streamed output into time-based files.
  • A Live controller endpoint that replays cached sections progressively.
  • Per-request CSP nonce replacement during replay.

Confidence Score: 4/5

The cache generation and replay paths can publish broken streams, and the public delay can exhaust server threads.

  • Zero-delay generation can crash.
  • Failed or partial curl runs still publish cache files.
  • Timing buckets can assign React protocol chunks to the wrong section.
  • Regeneration can expose partially written files to requests.

react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb and react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake

Security Review

The new public cached-stream endpoint accepts an unbounded delay. Concurrent requests with very large values can occupy the application thread pool and cause denial of service.

Important Files Changed

Filename Overview
react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb Adds the cached streaming action, but its request-controlled delay is unbounded.
react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake Adds cache management tasks with invalid-input, subprocess, timing, and atomic-publication bugs.
react_on_rails_pro/spec/dummy/client/app/components/CacheSection.jsx Adds delayed server-side Suspense rendering for section content.
react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/SelectiveHydrationDemo.jsx Composes four client sections under independently delayed cache boundaries.
react_on_rails_pro/spec/dummy/app/views/pages/selective_hydration_demo.html.erb Adds the streaming demo view and forwards query-provided section delays.
react_on_rails_pro/spec/dummy/client/app/packs/server-bundle.js Updates generated server-bundle loading for the server component registration path.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Task as section_cache:generate
    participant Demo as Demo endpoint
    participant React as React stream
    participant Disk as Section cache
    participant Browser
    participant Cached as Cached endpoint

    Task->>Demo: GET with section delays
    Demo->>React: Render Suspense boundaries
    React-->>Task: Shell and reveal chunks
    Task->>Disk: Bucket chunks by arrival time
    Browser->>Cached: GET with replay delay
    Cached->>Disk: Read section files
    Disk-->>Cached: Cached HTML fragments
    Cached-->>Browser: Stream fragments with current nonce
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Task as section_cache:generate
    participant Demo as Demo endpoint
    participant React as React stream
    participant Disk as Section cache
    participant Browser
    participant Cached as Cached endpoint

    Task->>Demo: GET with section delays
    Demo->>React: Render Suspense boundaries
    React-->>Task: Shell and reveal chunks
    Task->>Disk: Bucket chunks by arrival time
    Browser->>Cached: GET with replay delay
    Cached->>Disk: Read section files
    Disk-->>Cached: Cached HTML fragments
    Cached-->>Browser: Stream fragments with current nonce
Loading

Reviews (1): Last reviewed commit: "Add CacheSection component and section c..." | Re-trigger Greptile

def selective_hydration_cached # rubocop:disable Metrics/AbcSize
# Stream pre-cached section files with delays using ActionController::Live
# This simulates serving cached SSR content with progressive streaming
delay_seconds = (params[:delay] || 5).to_i

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security Unbounded Delay Exhausts Threads

The public delay parameter controls three blocking sleeps without an upper bound. Concurrent requests such as ?delay=999999 can retain all available Live/Puma threads for days and prevent the app from serving other requests.

Suggested change
delay_seconds = (params[:delay] || 5).to_i
delay_seconds = Integer(params[:delay] || 5, exception: false)&.clamp(0, 30) || 5

Context Used: CLAUDE.md (source)


# Determine which section this chunk belongs to based on time
# Section N receives content arriving at time >= N * delay_seconds
section_index = [(elapsed / delay_seconds).floor, section_count - 1].min

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Zero Delay Crashes Bucketing

The task accepts delay_seconds=0, but the first response chunk makes elapsed / delay_seconds infinite and .floor raises FloatDomainError. The advertised generator then exits before writing a usable cache; non-positive delays and section counts need validation before capture starts.

Context Used: CLAUDE.md (source)

total_timeout = (section_count * delay_seconds) + 30

# Use IO.popen with unbuffered curl for streaming
IO.popen(["curl", "-sN", "--max-time", total_timeout.to_s, uri.to_s], "r") do |io|

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Failed Capture Reports Success

The subprocess status is never checked. If curl cannot connect or times out after a partial response, the task still writes empty or truncated section files and prints that generation completed, so the cached endpoint later serves broken HTML or incomplete React reveal scripts.

Context Used: CLAUDE.md (source)

Comment on lines +72 to +74
section_index = [(elapsed / delay_seconds).floor, section_count - 1].min

sections[section_index] << chunk

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Arrival Time Mislabels Sections

Wall-clock windows do not preserve React boundary identity: server startup, scheduling, and transport buffering can move a chunk into a later bucket, and one chunk may contain multiple boundary reveals. Replaying those inferred files can reveal a section at the wrong time or leave a fallback permanently pending; the capture needs explicit section markers or independently captured boundaries.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

sections.each_with_index do |content, index|
filename = "section#{index}.html"
filepath = output_dir.join(filename)
File.write(filepath, content.force_encoding("UTF-8"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Live Cache Files Are Truncated

File.write truncates each file that the cached endpoint reads without synchronization. A request during regeneration can read an empty or partially written $RC script and receive malformed streamed HTML; generate into temporary files or a temporary directory and publish the complete set atomically.

Context Used: CLAUDE.md (source)

@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: 0b88c86f4a

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


# Determine which section this chunk belongs to based on time
# Section N receives content arriving at time >= N * delay_seconds
section_index = [(elapsed / delay_seconds).floor, section_count - 1].min

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject a zero section delay before dividing by it

When callers use delay_seconds=0 to generate an immediately available cache (a valid counterpart to the cached endpoint's ?delay=0), elapsed / delay_seconds becomes Infinity and .floor raises FloatDomainError as soon as curl emits the first chunk. Validate that the delay is positive or handle the zero-delay case without time-window division.

Useful? React with 👍 / 👎.

Comment on lines +58 to +76
IO.popen(["curl", "-sN", "--max-time", total_timeout.to_s, uri.to_s], "r") do |io|
# Read in small chunks for better timing accuracy
while (chunk = io.read_nonblock(4096, exception: false))
break if chunk.nil? # EOF

if chunk == :wait_readable
io.wait_readable(0.01)
next
end

elapsed = Time.now - section_start_time

# Determine which section this chunk belongs to based on time
# Section N receives content arriving at time >= N * delay_seconds
section_index = [(elapsed / delay_seconds).floor, section_count - 1].min

sections[section_index] << chunk
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fail cache generation when curl cannot complete

When the dummy server is unavailable, returns a connection failure, or exceeds --max-time, curl exits nonzero but its status is never checked. The task then writes empty or partial section*.html files and reports completion, replacing a previously usable cache; the cached route will subsequently stream corrupted content. Check curl's exit status before writing the section files.

Useful? React with 👍 / 👎.

@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: 7

🤖 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 `@react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb`:
- Line 307: Update the delay parsing near delay_seconds in the streaming action
to use Integer(..., exception: false), handle invalid or negative values safely,
and clamp valid delays to a small demo maximum so sleep never receives negatives
or unbounded durations. Preserve the existing default delay when the parameter
is missing or invalid.
- Line 17: Remove ActionController::Live from PagesController and move
selective_hydration_cached into a separate small controller that inherits from
PagesController or the appropriate base and includes ActionController::Live.
Update routing or references to target the new Live-only controller while
keeping all other PagesController actions on the normal request path.

In `@react_on_rails_pro/spec/dummy/client/app/components/CacheSection.jsx`:
- Around line 25-27: Update the Promise executor in the delay logic within
CacheSection so the setTimeout call is enclosed in a block body and does not
implicitly return its timeout ID, while preserving the existing delay behavior.

In `@react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake`:
- Line 114: Update the cache-generation instructions in section_cache.rake at
lines 114-114 and pages_controller.rb at lines 316-316 to invoke the task with
bundle exec, preserving the existing task names and arguments.
- Around line 57-76: The streaming fetch around IO.popen must validate curl’s
child exit status before publishing cache output. Capture the process status
from the IO.popen block, detect non-successful termination including timeout or
connection failure, and abort the task before writing any partial or empty cache
files; preserve normal section processing for successful fetches.
- Around line 176-181: Harden the :clean task’s output_dir handling by rejecting
route values containing "." or ".." path segments, then expand the candidate
path and verify it remains beneath Rails.root.join("public", "cache") before
calling FileUtils.rm_rf. Preserve the existing cleanup behavior for valid routes
and skip deletion when validation fails.
- Around line 90-100: Update the section cache generation flow around the output
directory and sections.each_with_index to write all captured files into a
uniquely named temporary directory, then atomically replace the existing route
cache directory only after every write succeeds. Ensure failures leave the
existing cache intact and remove or clean up the temporary directory as
appropriate.
🪄 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: 6c7f9d90-0bdb-47ae-b69c-6824306bc854

📥 Commits

Reviewing files that changed from the base of the PR and between 18bdc4f and 0b88c86.

📒 Files selected for processing (11)
  • react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb
  • react_on_rails_pro/spec/dummy/app/views/pages/selective_hydration_demo.html.erb
  • react_on_rails_pro/spec/dummy/client/app/components/CacheSection.jsx
  • react_on_rails_pro/spec/dummy/client/app/components/SelectiveHydration/SelectiveHydrationArticleSection.jsx
  • react_on_rails_pro/spec/dummy/client/app/components/SelectiveHydration/SelectiveHydrationCategoriesSection.jsx
  • react_on_rails_pro/spec/dummy/client/app/components/SelectiveHydration/SelectiveHydrationFooterSection.jsx
  • react_on_rails_pro/spec/dummy/client/app/components/SelectiveHydration/SelectiveHydrationHeaderSection.jsx
  • react_on_rails_pro/spec/dummy/client/app/packs/server-bundle.js
  • react_on_rails_pro/spec/dummy/client/app/ror-auto-load-components/SelectiveHydrationDemo.jsx
  • react_on_rails_pro/spec/dummy/config/routes.rb
  • react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake

# https://github.com/shakacode/react_on_rails/blob/main/REACT-ON-RAILS-PRO-LICENSE.md

class PagesController < ApplicationController # rubocop:disable Metrics/ClassLength
include ActionController::Live

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

rg -n -C3 'include ActionController::Live|class PagesController' \
  react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb
fd -a 'Gemfile.lock' . --exec rg -n 'actionpack \(' {}

Repository: shakacode/react_on_rails

Length of output: 2079


🏁 Script executed:

#!/bin/bash
set -eu

file='react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb'

# Show the relevant controller sections around the Live include and the line numbers mentioned in the review.
sed -n '1,60p' "$file"
printf '\n---\n'
sed -n '290,330p' "$file"
printf '\n---\n'
sed -n '330,360p' "$file"

Repository: shakacode/react_on_rails

Length of output: 5216


Isolate ActionController::Live to the streaming action.
react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb:17 — including ActionController::Live here changes the threading model for every PagesController action. Move selective_hydration_cached into a small Live-only controller so the rest of the controller stays on the normal request path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb` at line
17, Remove ActionController::Live from PagesController and move
selective_hydration_cached into a separate small controller that inherits from
PagesController or the appropriate base and includes ActionController::Live.
Update routing or references to target the new Live-only controller while
keeping all other PagesController actions on the normal request path.

def selective_hydration_cached # rubocop:disable Metrics/AbcSize
# Stream pre-cached section files with delays using ActionController::Live
# This simulates serving cached SSR content with progressive streaming
delay_seconds = (params[:delay] || 5).to_i

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound and validate the streaming delay.

A negative value raises from sleep, while an arbitrarily large value occupies a Live worker thread indefinitely. Parse with Integer(..., exception: false) and clamp to a small demo limit.

Proposed validation
-    delay_seconds = (params[:delay] || 5).to_i
+    requested_delay = Integer(params.fetch(:delay, 5), exception: false)
+    delay_seconds = (requested_delay || 5).clamp(0, 30)
📝 Committable suggestion

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

Suggested change
delay_seconds = (params[:delay] || 5).to_i
requested_delay = Integer(params.fetch(:delay, 5), exception: false)
delay_seconds = (requested_delay || 5).clamp(0, 30)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb` at line
307, Update the delay parsing near delay_seconds in the streaming action to use
Integer(..., exception: false), handle invalid or negative values safely, and
clamp valid delays to a small demo maximum so sleep never receives negatives or
unbounded durations. Preserve the existing default delay when the parameter is
missing or invalid.

Comment on lines +25 to +27
if (delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix ESLint no-promise-executor-return error.

Arrow functions without braces implicitly return the result of their expression. setTimeout returns a timeout ID, which violates the rule that promise executors should return void. Wrap the setTimeout call in braces to avoid returning a value.

🛠️ Proposed fix
   if (delayMs > 0) {
-    await new Promise((resolve) => setTimeout(resolve, delayMs));
+    await new Promise((resolve) => {
+      setTimeout(resolve, delayMs);
+    });
   }
📝 Committable suggestion

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

Suggested change
if (delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
if (delayMs > 0) {
await new Promise((resolve) => {
setTimeout(resolve, delayMs);
});
}
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 25-25: Avoid using the initial state variable in setState
Context: setTimeout(resolve, delayMs)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🪛 ESLint

[error] 26-26: Return values from promise executor functions cannot be read.

(no-promise-executor-return)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/spec/dummy/client/app/components/CacheSection.jsx` around
lines 25 - 27, Update the Promise executor in the delay logic within
CacheSection so the setTimeout call is enclosed in a block body and does not
implicitly return its timeout ID, while preserving the existing delay behavior.

Comment on lines +57 to +76
# Use IO.popen with unbuffered curl for streaming
IO.popen(["curl", "-sN", "--max-time", total_timeout.to_s, uri.to_s], "r") do |io|
# Read in small chunks for better timing accuracy
while (chunk = io.read_nonblock(4096, exception: false))
break if chunk.nil? # EOF

if chunk == :wait_readable
io.wait_readable(0.01)
next
end

elapsed = Time.now - section_start_time

# Determine which section this chunk belongs to based on time
# Section N receives content arriving at time >= N * delay_seconds
section_index = [(elapsed / delay_seconds).floor, section_count - 1].min

sections[section_index] << chunk
end
end

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Abort when the streaming fetch fails.

curl may time out or fail to connect, but its exit status is ignored; the task then writes partial or empty files and reports success. Check the child status before publishing any cache output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake` around lines 57 -
76, The streaming fetch around IO.popen must validate curl’s child exit status
before publishing cache output. Capture the process status from the IO.popen
block, detect non-successful termination including timeout or connection
failure, and abort the task before writing any partial or empty cache files;
preserve normal section processing for successful fetches.

Comment on lines +90 to +100
# Create output directory
output_dir = Rails.root.join("public", "cache", route.gsub(%r{^/}, "").tr("/", "_"))
FileUtils.mkdir_p(output_dir)

# Write each section (force UTF-8 encoding)
sections.each_with_index do |content, index|
filename = "section#{index}.html"
filepath = output_dir.join(filename)
File.write(filepath, content.force_encoding("UTF-8"))
puts "Wrote #{filepath} (#{content.bytesize} bytes)"
end

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Replace the cache directory atomically.

Regenerating with a smaller section_count leaves old sectionN.html files behind, and cached replay will continue streaming them. Write into a temporary directory, then replace the existing directory only after successful capture.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake` around lines 90 -
100, Update the section cache generation flow around the output directory and
sections.each_with_index to write all captured files into a uniquely named
temporary directory, then atomically replace the existing route cache directory
only after every write succeeds. Ensure failures leave the existing cache intact
and remove or clean up the temporary directory as appropriate.

output_dir = Rails.root.join("public", "cache", route.gsub(%r{^/}, "").tr("/", "_"))

unless Dir.exist?(output_dir)
abort "Output directory not found: #{output_dir}\nRun 'rake section_cache:generate' first."

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use bundle exec in both cache-generation instructions.

  • react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake#L114-L114: use bundle exec rake section_cache:generate.
  • react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb#L316-L316: use bundle exec rake section_cache:generate[...].

As per coding guidelines, “Use bundle exec for Ruby commands.” <coding_guidelines>

📍 Affects 2 files
  • react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake#L114-L114 (this comment)
  • react_on_rails_pro/spec/dummy/app/controllers/pages_controller.rb#L316-L316
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake` at line 114,
Update the cache-generation instructions in section_cache.rake at lines 114-114
and pages_controller.rb at lines 316-316 to invoke the task with bundle exec,
preserving the existing task names and arguments.

Source: Coding guidelines

Comment on lines +176 to +181
task :clean, [:route] => :environment do |_t, args|
route = args[:route] || "/selective_hydration_demo"
output_dir = Rails.root.join("public", "cache", route.gsub(%r{^/}, "").tr("/", "_"))

if Dir.exist?(output_dir)
FileUtils.rm_rf(output_dir)

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent cleanup from escaping the section-cache directory.

With route="..", output_dir resolves to public/cache/.., and rm_rf deletes the entire public directory. Reject dot segments and verify the expanded path remains beneath public/cache before deletion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails_pro/spec/dummy/lib/tasks/section_cache.rake` around lines 176
- 181, Harden the :clean task’s output_dir handling by rejecting route values
containing "." or ".." path segments, then expand the candidate path and verify
it remains beneath Rails.root.join("public", "cache") before calling
FileUtils.rm_rf. Preserve the existing cleanup behavior for valid routes and
skip deletion when validation fails.

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.

1 participant