Skip to content

Conversation

@AbanoubGhadban
Copy link
Collaborator

@AbanoubGhadban AbanoubGhadban commented Jan 13, 2026

Summary

Fixes two race conditions causing async props to not be delivered when emit.call is invoked without delays.

Fix 1: Lazy AsyncPropsManager initialization

  • Update chunks could arrive before AsyncPropsManager was initialized
  • Added getOrCreateAsyncPropsManager() for lazy initialization on first access

Fix 2: Await response stream completion

  • handleIncrementalRenderStream returned before HTTP response was fully sent
  • Changed void onResponseStart() to capture and await the promise

Fixes #2270

Test plan

  • Test stream_react_component_with_async_props without sleep delays
  • Verify async props are delivered correctly regardless of timing

🤖 Generated with Claude Code

AbanoubGhadban and others added 2 commits January 13, 2026 14:53
Implements lazy initialization pattern for AsyncPropsManager to handle
race conditions between initial render requests and update chunks.

The new getOrCreateAsyncPropsManager function:
- Accepts sharedExecutionContext as parameter
- Creates or retrieves existing AsyncPropsManager instance
- Ensures same manager is used regardless of which code executes first

This fixes the bug where commenting out sleep statements in
test_incremental_rendering.html.erb caused async props not to be sent,
because update chunks were arriving before the manager was set up.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Remove unused log import (replaced by console.log for debugging)
- Change ++ operators to += 1 to satisfy no-plusplus rule

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 13, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/getOrCreateAsyncPropsManager-lazy-init

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

❤️ Share

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

@claude
Copy link

claude bot commented Jan 13, 2026

Code Review: Fix async props race condition with lazy AsyncPropsManager initialization

Summary

This PR effectively addresses the race condition described in #2270 by implementing lazy initialization of AsyncPropsManager through a getOrCreateAsyncPropsManager function. The fix ensures update chunks can arrive and execute before the initial render completes without losing async props.


✅ Strengths

1. Excellent Architecture & Documentation

  • The implementation is well-documented with clear design explanations in both TypeScript and Ruby code
  • The AsyncPropsManager.ts header (lines 28-57) provides excellent context about promise caching, order independence, and stream lifecycle
  • Ruby code includes helpful protocol documentation explaining NDJSON communication between Rails and Node

2. Clean Lazy Initialization Pattern

  • getOrCreateAsyncPropsManager() function (AsyncPropsManager.ts:152-164) implements the singleton pattern correctly
  • Uses sharedExecutionContext Map for request-scoped state (avoiding global variables that would leak across requests)
  • Both initial render and update chunks use the same function, ensuring they always get the same manager instance

3. Minimal API Surface Changes

  • Only adds one new exported function (getOrCreateAsyncPropsManager)
  • Ruby-side changes are minimal (just switching from direct manager creation to lazy getter)
  • Type definitions properly updated (types/index.ts:142-146, 520)

4. Good Error Handling

  • Maintains proper rejection of unresolved props when stream ends
  • Handles the case where manager doesn't exist (returns undefined when stream is closed)

⚠️ Issues & Concerns

1. CRITICAL: Missing Test Coverage
This PR has NO test coverage for the race condition fix. The "Test plan" checkbox in the PR description is unchecked:

  • Test stream_react_component_with_async_props without sleep delays before emit calls
  • Verify async props are delivered correctly regardless of timing

Per CLAUDE.md guidelines:

CRITICAL - LOCAL TESTING REQUIREMENTS:

  1. NEVER claim a test is "fixed" without running it locally first

Recommendation: Before merging, you should:

  1. Add an integration test that calls emit.call immediately (without sleep) to verify props are delivered
  2. Add a unit test for getOrCreateAsyncPropsManager that verifies it returns the same instance on multiple calls
  3. Consider adding a test that simulates update chunks arriving before the initial render

2. Potential Memory Leak Concern
The AsyncPropsManager is stored in sharedExecutionContext but there's no explicit cleanup mechanism shown. Questions:

  • Is sharedExecutionContext automatically garbage collected after the HTTP request completes?
  • Should there be an explicit cleanup call somewhere?

Looking at handleIncrementalRenderRequest.ts, the executionContext appears to be scoped to the request lifecycle, which should be fine. But it would be good to verify this doesn't leak memory in long-running processes.

3. Debug Console.log Cleanup
The PR description mentions "Removes debug console.log statements" but I don't see these in the diff. Were they already removed, or is this still pending?

4. Type Safety Consideration
In AsyncPropsManager.ts:155, the type assertion could be safer:

let manager = sharedExecutionContext.get(ASYNC_PROPS_MANAGER_KEY) as AsyncPropsManager | undefined;

Consider using type guard instead:

const manager = sharedExecutionContext.get(ASYNC_PROPS_MANAGER_KEY);
if (manager && manager instanceof AsyncPropsManager) {
  return manager;
}

This provides runtime type checking, not just compile-time assertions.


🔍 Code Quality

JavaScript/TypeScript:

  • ✅ Well-documented with JSDoc comments
  • ✅ Proper TypeScript types
  • ✅ Follows functional programming patterns
  • ✅ No linting issues visible

Ruby:

  • ✅ Follows Ruby style conventions
  • ✅ Good use of heredocs for JavaScript generation
  • ✅ Proper error handling in AsyncPropsEmitter.call

🛡️ Security Considerations

No security concerns identified:

  • Uses JSON serialization properly
  • No eval() or unsafe code execution
  • Request-scoped state prevents cross-request data leakage

📊 Performance Considerations

Positive:

  • Lazy initialization means no overhead if async props aren't used
  • Single manager instance per request (no redundant object creation)

Neutral:

  • Map lookup on every getOrCreateAsyncPropsManager call is O(1) and negligible

📝 Missing from PR

  1. Tests (as mentioned above)

  2. CHANGELOG entry - Per CLAUDE.md:

    Update CHANGELOG.md for user-visible changes only (features, bug fixes, breaking changes...)

    This is a bug fix that affects Pro users, so it should be added to CHANGELOG_PRO.md.

  3. RuboCop/Prettier check - Per CLAUDE.md:

    BEFORE EVERY COMMIT/PUSH:

    1. ALWAYS run bundle exec rubocop and fix ALL violations
    2. ALWAYS ensure files end with a newline character

    Please verify these were run locally.


🎯 Recommendations Before Merge

  1. Add test coverage (integration test for race condition scenario)
  2. Add CHANGELOG_PRO.md entry
  3. Verify linting passes (bundle exec rubocop and pnpm run format.listDifferent)
  4. Manual testing - Test with and without sleep delays to verify fix works
  5. Consider type guard instead of type assertion for runtime safety

Final Verdict

✅ Approve with required changes

The core fix is sound and well-implemented. The lazy initialization pattern correctly solves the race condition. However, test coverage is mandatory before merging to ensure this fix works and prevent regressions.

The code quality is high, documentation is excellent, and the approach is architecturally clean. Once tests are added and the checklist items completed, this is ready to merge.

Great work on the analysis and fix! 🎉

@github-actions
Copy link
Contributor

github-actions bot commented Jan 13, 2026

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.53 KB (0%)
react-on-rails/client bundled (gzip) (time) 62.53 KB (0%)
react-on-rails/client bundled (brotli) 53.71 KB (0%)
react-on-rails/client bundled (brotli) (time) 53.71 KB (0%)
react-on-rails-pro/client bundled (gzip) 63.53 KB (+0.06% 🔺)
react-on-rails-pro/client bundled (gzip) (time) 63.53 KB (+0.06% 🔺)
react-on-rails-pro/client bundled (brotli) 54.59 KB (+0.05% 🔺)
react-on-rails-pro/client bundled (brotli) (time) 54.56 KB (-0.02% 🔽)
registerServerComponent/client bundled (gzip) 127.42 KB (+0.03% 🔺)
registerServerComponent/client bundled (gzip) (time) 127.42 KB (+0.03% 🔺)
registerServerComponent/client bundled (brotli) 61.52 KB (+0.12% 🔺)
registerServerComponent/client bundled (brotli) (time) 61.52 KB (+0.12% 🔺)
wrapServerComponentRenderer/client bundled (gzip) 121.96 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 121.96 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.66 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.66 KB (0%)

@AbanoubGhadban AbanoubGhadban force-pushed the fix/getOrCreateAsyncPropsManager-lazy-init branch from 5efcf80 to 455e7d4 Compare January 13, 2026 13:47
@greptile-apps
Copy link

greptile-apps bot commented Jan 13, 2026

Greptile Overview

Greptile Summary

This PR successfully addresses a race condition in async props handling where update chunks could arrive before AsyncPropsManager initialization. The solution introduces a lazy initialization pattern via getOrCreateAsyncPropsManager() that ensures whichever code path executes first (initial render or update chunk) will create the manager, and subsequent calls will reuse it.

Key Changes

Core Fix:

  • Added getOrCreateAsyncPropsManager(sharedExecutionContext) function that implements lazy initialization
  • Updated addAsyncPropsCapabilityToComponentProps to accept sharedExecutionContext parameter and use the lazy initialization pattern
  • Modified update chunk generation in Ruby to call getOrCreateAsyncPropsManager instead of directly accessing sharedExecutionContext.get("asyncPropsManager")

Architecture:
The fix leverages the fact that sharedExecutionContext is a Map scoped to a single HTTP request within an ExecutionContext. Both the initial render request and subsequent update chunks share the same ExecutionContext, allowing them to safely share the AsyncPropsManager instance without race conditions.

Issues Found

Critical Issue:
A test fixture (asyncComponentsTreeForTestingRenderingRequest.js) still uses the old API signature where addAsyncPropsCapabilityToComponentProps returned { asyncPropManager, props }. The new API returns only { props } and requires sharedExecutionContext as a parameter. This will cause test failures and must be updated before merging.

Code Quality:

  • Removed debug console.log and log.info statements as intended
  • Improved error logging in async_props_emitter.rb
  • Simplified error handling by relying on VM error reporting

Race Condition Resolution

The lazy initialization pattern correctly handles all timing scenarios:

  1. Initial render first: Creates manager, stores in context, update chunks reuse it
  2. Update chunk first: Creates manager, stores in context, initial render reuses it
  3. Concurrent access: First call creates manager, subsequent calls return existing instance

The implementation is thread-safe because sharedExecutionContext is not shared across requests - each HTTP request gets its own isolated ExecutionContext.

Confidence Score: 2/5

  • This PR has a critical test fixture bug that must be fixed before merging
  • While the core race condition fix is architecturally sound and well-implemented, there is a critical bug in the test fixture that uses the old API signature. This will cause test failures and indicates the changes weren't fully tested. The test fixture at asyncComponentsTreeForTestingRenderingRequest.js uses the old addAsyncPropsCapabilityToComponentProps API (expecting { asyncPropManager, props } return value) instead of the new API (requiring sharedExecutionContext parameter and returning only { props }). This must be fixed before the PR can be safely merged.
  • Critical: packages/react-on-rails-pro-node-renderer/tests/fixtures/projects/spec-dummy/asyncComponentsTreeForTestingRenderingRequest.js - Must be updated to use new API signature

Important Files Changed

File Analysis

Filename Score Overview
packages/react-on-rails-pro/src/AsyncPropsManager.ts 5/5 Adds getOrCreateAsyncPropsManager function for lazy initialization - clean implementation that properly handles race conditions
packages/react-on-rails-pro/src/ReactOnRailsRSC.ts 5/5 Updates addAsyncPropsCapabilityToComponentProps to use getOrCreateAsyncPropsManager and accept sharedExecutionContext parameter - properly implements lazy initialization
react_on_rails_pro/lib/react_on_rails_pro/async_props_emitter.rb 5/5 Updates generated JS to use getOrCreateAsyncPropsManager instead of direct sharedExecutionContext access - correctly implements race condition fix with improved logging
react_on_rails_pro/lib/react_on_rails_pro/server_rendering_js_code.rb 5/5 Updates async_props_setup_js to pass sharedExecutionContext to addAsyncPropsCapabilityToComponentProps and removes manual manager storage - properly aligns with new API
packages/react-on-rails-pro-node-renderer/tests/fixtures/projects/spec-dummy/asyncComponentsTreeForTestingRenderingRequest.js 0/5 Test fixture uses OLD API signature - critical bug that will cause test failures, needs immediate update to use new addAsyncPropsCapabilityToComponentProps signature

Sequence Diagram

sequenceDiagram
    participant Rails as Rails Server
    participant Worker as Node Worker
    participant VM as VM Context
    participant Manager as AsyncPropsManager
    participant React as React Component

    Note over Rails,React: Race Condition Fix: Lazy Initialization Pattern

    Rails->>Worker: POST /incremental-render<br/>(First NDJSON line: renderingRequest)
    Worker->>VM: handleIncrementalRenderRequest()
    VM->>VM: buildExecutionContext()<br/>Creates sharedExecutionContext Map
    
    Note over VM,Manager: INITIAL RENDER PATH
    VM->>VM: Execute renderingRequest JS
    VM->>Manager: getOrCreateAsyncPropsManager(sharedExecutionContext)
    alt Manager doesn't exist
        Manager->>Manager: new AsyncPropsManager()
        Manager->>VM: Store in sharedExecutionContext["asyncPropsManager"]
    end
    VM->>React: addAsyncPropsCapabilityToComponentProps(props, sharedExecutionContext)
    React->>Manager: getReactOnRailsAsyncProp("users")
    Manager->>React: Returns Promise (suspends)
    React-->>Worker: Initial HTML shell
    
    par Update Chunks Can Arrive Anytime
        Note over Rails,Manager: UPDATE CHUNK PATH (can race with initial render)
        Rails->>Worker: NDJSON line 2: updateChunk for "users"
        Worker->>VM: sink.add(updateChunk)
        VM->>VM: Execute updateChunk JS
        VM->>Manager: getOrCreateAsyncPropsManager(sharedExecutionContext)
        alt Manager already exists
            Manager->>Manager: Return existing instance
        else Manager doesn't exist yet (race condition)
            Manager->>Manager: new AsyncPropsManager()
            Manager->>VM: Store in sharedExecutionContext["asyncPropsManager"]
        end
        VM->>Manager: setProp("users", userData)
        Manager->>React: Resolve Promise
        React-->>Worker: More HTML chunks
    end
    
    Rails->>Worker: Close HTTP request
    Worker->>VM: sink.handleRequestClosed()
    VM->>Manager: getOrCreateAsyncPropsManager(sharedExecutionContext)
    VM->>Manager: endStream()
    Manager->>Manager: Reject unresolved promises
    React-->>Worker: Final HTML chunks
Loading

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Jan 13, 2026

Additional Comments (1)

packages/react-on-rails-pro-node-renderer/tests/fixtures/projects/spec-dummy/asyncComponentsTreeForTestingRenderingRequest.js
This test fixture uses the OLD API signature for addAsyncPropsCapabilityToComponentProps, which is incompatible with the changes in this PR. The function signature has changed:

Old (what this fixture uses):

var { props: propsWithAsyncProps, asyncPropManager } = 
  ReactOnRails.addAsyncPropsCapabilityToComponentProps(usedProps);

New (what it should be):

var { props: propsWithAsyncProps } = 
  ReactOnRails.addAsyncPropsCapabilityToComponentProps(usedProps, sharedExecutionContext);

The new signature:

  1. Requires sharedExecutionContext as a second parameter
  2. No longer returns asyncPropManager (it's managed internally via getOrCreateAsyncPropsManager)
  3. The manual sharedExecutionContext.set("asyncPropsManager", asyncPropManager) line is no longer needed
  if (ReactOnRails.isRSCBundle) {
    var { props: propsWithAsyncProps } = ReactOnRails.addAsyncPropsCapabilityToComponentProps(usedProps, sharedExecutionContext);
    usedProps = propsWithAsyncProps;
  }

This fixture needs to be updated to match the new API, otherwise tests using this fixture will fail.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-on-rails-pro-node-renderer/tests/fixtures/projects/spec-dummy/asyncComponentsTreeForTestingRenderingRequest.js
Line: 23:25

Comment:
This test fixture uses the OLD API signature for `addAsyncPropsCapabilityToComponentProps`, which is incompatible with the changes in this PR. The function signature has changed:

**Old (what this fixture uses):**
```javascript
var { props: propsWithAsyncProps, asyncPropManager } = 
  ReactOnRails.addAsyncPropsCapabilityToComponentProps(usedProps);
```

**New (what it should be):**
```javascript
var { props: propsWithAsyncProps } = 
  ReactOnRails.addAsyncPropsCapabilityToComponentProps(usedProps, sharedExecutionContext);
```

The new signature:
1. Requires `sharedExecutionContext` as a second parameter
2. No longer returns `asyncPropManager` (it's managed internally via `getOrCreateAsyncPropsManager`)
3. The manual `sharedExecutionContext.set("asyncPropsManager", asyncPropManager)` line is no longer needed

```suggestion
  if (ReactOnRails.isRSCBundle) {
    var { props: propsWithAsyncProps } = ReactOnRails.addAsyncPropsCapabilityToComponentProps(usedProps, sharedExecutionContext);
    usedProps = propsWithAsyncProps;
  }
```

This fixture needs to be updated to match the new API, otherwise tests using this fixture will fail.

How can I resolve this? If you propose a fix, please make it concise.

@claude
Copy link

claude bot commented Jan 13, 2026

Code Review - PR #2297

Summary

This PR fixes a race condition in async props handling by implementing lazy initialization of the AsyncPropsManager. The core issue is that update chunks could arrive before the initial render creates the manager, causing async props to be lost.


✅ Strengths

1. Solid Architecture - Lazy Initialization Pattern

The getOrCreateAsyncPropsManager function is well-designed:

  • Uses the shared execution context as the source of truth
  • Handles race conditions elegantly (whoever arrives first creates it)
  • Simple and maintainable implementation

2. Excellent Documentation

The JSDoc comments clearly explain:

  • The race condition being solved
  • How the lazy initialization works
  • The relationship with sharedExecutionContext

This is particularly well done in server_rendering_js_code.rb:61-67.

3. Type Safety

Proper TypeScript types added to index.ts with clear parameter documentation.

4. Backward Compatibility

The change is internal - no breaking changes to the public API.


⚠️ Issues & Recommendations

1. CRITICAL: Missing Test Coverage

The PR description shows:

## Test plan
- [ ] Test `stream_react_component_with_async_props` without sleep delays
- [ ] Verify async props are delivered correctly regardless of timing

Both checkboxes are unchecked. Per CLAUDE.md:

CRITICAL - LOCAL TESTING REQUIREMENTS:

  1. NEVER claim a test is "fixed" without running it locally first

Recommendation:

  • Add a test case that reproduces the race condition (e.g., immediate emit without sleep)
  • Verify the fix works locally before merging
  • Consider adding this to react_on_rails_pro/spec/react_on_rails_pro/async_props_emitter_spec.rb

2. Excessive Logging in Production Code

In async_props_emitter.rb:34-40:

Rails.logger.info do
  "[ReactOnRailsPro::AsyncProps] Sending prop '\#{prop_name}' (\#{chunk_json.bytesize} bytes)..."
end
@request_stream << chunk_json
Rails.logger.info do
  "[ReactOnRailsPro::AsyncProps] Sent prop '\#{prop_name}' successfully"
end

Issues:

  • Two info-level logs for EVERY async prop sent (could be dozens per request)
  • Will clutter production logs significantly
  • "Sent successfully" log doesn't add value - it just means the stream write didn't raise

Recommendations:

  • Change to debug level: Rails.logger.debug
  • Remove the "sent successfully" log entirely
  • Keep only the error logging (which is well-done)

3. Inconsistent Import Order

In handleIncrementalRenderRequest.ts:1-4:

-import log from '../shared/log';
 import { getRequestBundleFilePath } from '../shared/utils';
+import log from '../shared/log';

Why this change? It seems unrelated to the PR's purpose. If it's a linter fix, that's fine, but:

  • The PR description doesn't mention import reordering
  • This should ideally be in a separate commit or explained

4. Minor: Whitespace Change

In handleIncrementalRenderStream.ts:66:

 let boundary = buffer.indexOf('\n');
+
 while (boundary !== -1) {

Again, not directly related to the fix. Consider whether these formatting changes should be in the scope.


🔍 Security & Performance

Security: ✅ Good

  • No security concerns identified
  • The sharedExecutionContext is properly scoped to the request
  • No global state pollution

Performance: ✅ Good

  • Lazy initialization adds minimal overhead (one Map lookup per request)
  • No memory leaks - manager is scoped to request lifecycle

📋 Testing Recommendations

Add a test case like this (pseudo-code):

# In async_props_emitter_spec.rb
it 'handles race condition when emit is called before initial render' do
  # Simulate update chunk arriving before initial render
  emit.call('users', [{ name: 'Alice' }])
  
  # Now render the component
  result = stream_react_component_with_async_props(...)
  
  # Verify the prop was delivered
  expect(result).to include('Alice')
end

🎯 Before Merging

Per the repository guidelines:

  1. ✅ Run linters:

    bundle exec rubocop
    pnpm run format.listDifferent
  2. ⚠️ Complete the test plan:

    • Test the race condition scenario locally
    • Update the checkboxes in the PR description
  3. 🔄 Consider addressing the logging verbosity to avoid production log pollution

  4. 📝 Update CHANGELOG_PRO.md if not already done (this is a bug fix in Pro functionality)


Overall Assessment

Rating: ⭐⭐⭐⭐ (4/5)

The core fix is architecturally sound and solves a real problem elegantly. The main concerns are:

  • Missing test verification (critical per CLAUDE.md)
  • Excessive logging that will clutter production logs
  • Minor formatting changes that obscure the actual fix

Once the test plan is completed and logging is adjusted, this will be a solid merge.

Great work identifying and fixing this race condition! 🚀

Remove debugging logs from:
- AsyncPropsManager.ts
- handleIncrementalRenderRequest.ts
- handleIncrementalRenderStream.ts

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@AbanoubGhadban AbanoubGhadban force-pushed the fix/getOrCreateAsyncPropsManager-lazy-init branch from 455e7d4 to 30c296d Compare January 13, 2026 13:49
@claude
Copy link

claude bot commented Jan 13, 2026

Code Review - PR #2297

This PR successfully addresses a race condition in async props delivery by implementing lazy initialization of AsyncPropsManager. The fix is well-architected and follows good engineering principles.

Strengths

  1. Excellent Problem Analysis - The root cause identification is spot-on
  2. Clean Architecture - getOrCreateAsyncPropsManager() is idempotent and type-safe
  3. Consistent Implementation - Used correctly in both code paths
  4. Good Documentation - Comprehensive JSDoc and comments
  5. Improved Logging - Enhanced error messages with byte sizes and backtraces

Critical Issue: Missing Test Coverage

The existing test (test_incremental_rendering.html.erb) uses sleep 1 which MASKS the race condition this PR fixes. You need a test that exercises the race condition without sleep delays.

Please add a test that emits props immediately to verify the fix prevents regression.

Other Suggestions

  1. Parameter Validation - getOrCreateAsyncPropsManager should validate sharedExecutionContext is a valid Map
  2. Test Plan - Update PR description to check off completed tests or explain why manual testing wasn't performed

Verdict

Approved with suggestions. Core fix is solid, but please add the automated test before merging.

Code Quality: 8.5/10
Documentation: 9/10
Test Coverage: 4/10 (would be 9/10 with race condition test)

Great work identifying and fixing this subtle timing issue!

@claude
Copy link

claude bot commented Jan 13, 2026

Code Review - PR 2297

Overall Assessment

Strong fix for a critical race condition. The implementation is well-thought-out and follows good engineering practices.

Strengths

  1. Problem Identification - Correctly fixes the race condition where update chunks arrive before AsyncPropsManager initialization
  2. Code Quality - Clean, well-documented implementation with excellent JSDoc comments
  3. API Design - Backwards-compatible signature changes, simplified return value
  4. Error Handling - Improved error logging with backtrace

Issues and Recommendations

CRITICAL: Missing Test Coverage

The PR has NO tests for the race condition fix. This is a serious gap for a concurrency issue.

Required tests:

  1. TypeScript unit test for getOrCreateAsyncPropsManager
  2. Integration test for the actual race condition (test emit without sleep delays)

The test plan checkbox in the PR description is unfilled. This needs local testing before merge per CLAUDE.md guidelines.

MODERATE: RSpec Test Needs Update

The existing test at react_on_rails_pro/spec/react_on_rails_pro/server_rendering_js_code_spec.rb lines 38-39 and 84 checks for the OLD implementation and will likely fail because asyncPropManager variable no longer exists in generated JS.

Security and Performance

  • No security concerns
  • Performance is positive (eliminates wasted update chunks, O(1) Map lookup)

Before Merging Checklist

  • Add unit tests for getOrCreateAsyncPropsManager (TypeScript)
  • Add integration test for race condition scenario (Ruby/RSpec)
  • Update existing RSpec expectations (lines 38-40, 84)
  • Test the actual scenario from issue 2270 locally
  • Run bundle exec rubocop and pnpm run lint
  • Fill in test plan checkboxes

Conclusion

This is a solid fix for a real production issue, but it needs test coverage before merging. The race condition is subtle and could easily regress without tests.

Recommendation: Request changes for test coverage, then approve once tests are added.

Great work identifying and solving this timing issue!

Async props requires RSC bundle because getOrCreateAsyncPropsManager
is only implemented there (server bundle has a stub that throws).

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@claude
Copy link

claude bot commented Jan 13, 2026

Code Review Feedback

Overall Assessment

This PR effectively solves a critical race condition in async props handling. The lazy initialization pattern is a solid architectural choice that properly addresses the core issue where update chunks could arrive before the AsyncPropsManager was initialized.


Strengths

  1. Clean API Design: The getOrCreateAsyncPropsManager function follows the single responsibility principle and provides a clear contract.

  2. Good Documentation: JSDoc comments clearly explain the race condition handling and the purpose of the shared execution context.

  3. Backward Compatibility: The changes are additive and do not break existing functionality.

  4. Proper Error Handling: Enhanced error logging in async_props_emitter.rb with backtrace is helpful for debugging.

  5. Correct Bundle Selection: The fix to force RSC bundle when async_props_block is present is crucial and well-commented.


CRITICAL Issues Found

1. Outdated Test Assertion

File: react_on_rails_pro/spec/react_on_rails_pro/async_props_emitter_spec.rb:23

The test on line 23 still expects the OLD code pattern:

  • sharedExecutionContext.get("asyncPropsManager")

But the implementation NOW uses:

  • ReactOnRails.getOrCreateAsyncPropsManager(sharedExecutionContext)

This test will FAIL! It needs to be updated to expect the new function call pattern.

2. Missing Test Coverage

The PR test plan mentions testing stream_react_component_with_async_props without sleep delays, but:

  • Test checkboxes are unchecked
  • No new integration test verifies the race condition is fixed
  • The existing test file still has sleep 1 delays on lines 6 and 9

Recommendation: Add an integration test that calls stream_react_component_with_async_props with NO sleep delays and verifies async props are delivered correctly.

3. Type Safety Issue

File: packages/react-on-rails-pro/src/AsyncPropsManager.ts:158

The type assertion assumes the value is either AsyncPropsManager or undefined, but Map.get() could return ANY value if something else pollutes the key. Consider adding an instanceof check for defensive programming against accidental key collisions.


Security & Performance

Security:

  • No SQL injection or XSS vulnerabilities introduced
  • Proper error boundary handling
  • Minor concern: Shared execution context pollution (addressed by type safety suggestion)

Performance:

  • Lazy initialization reduces overhead
  • O(1) Map lookup
  • No memory leaks (manager lifecycle tied to request scope)

Required Actions Before Merge

  1. Fix the failing test in async_props_emitter_spec.rb:23
  2. Run the test suite locally: bundle exec rake run_rspec:dummy
  3. Add integration test for the no-sleep scenario (the core bug being fixed)
  4. Consider adding type guard in getOrCreateAsyncPropsManager
  5. Update test plan checkboxes or explain why manual testing was sufficient

Status: Needs work - Fix the test assertion before merging. Otherwise, solid implementation of the race condition fix.

Generated with Claude Code

@claude
Copy link

claude bot commented Jan 14, 2026

Code Review Summary

This PR effectively fixes the race condition in async props delivery by implementing lazy initialization of AsyncPropsManager. The approach is sound and well-documented.

Strengths

  1. Excellent Problem Analysis - Clear understanding of the race condition between initial render and update chunks
  2. Clean Implementation - The getOrCreateAsyncPropsManager() function is simple and focused
  3. Type Safety - TypeScript types properly updated
  4. Documentation - Excellent inline comments explaining race condition handling

Potential Issues & Recommendations

1. Test Coverage Gap (HIGH PRIORITY)

The existing test file (AsyncPropManager.test.ts) only tests the AsyncPropsManager class directly, but doesn't test the new getOrCreateAsyncPropsManager() function or the race condition scenario it solves.

Recommendation: Add tests that verify multiple calls return the same instance and that update chunks arriving before initial render work correctly.

2. RSpec Test Update Required (HIGH PRIORITY)

The spec file async_props_emitter_spec.rb:23 still checks for the old pattern sharedExecutionContext.get but the new code uses ReactOnRails.getOrCreateAsyncPropsManager(sharedExecutionContext).

This test will likely fail. Please update the expectation to match the new implementation.

3. Minor: Improved Error Message

The error message improvement in async_props_emitter.rb:37-38 is great, but consider making backtrace formatting more defensive to avoid nil values.

4. Documentation: Bundle Hash Change

The change in request.rb:100 from bundle_timestamp to pool.rsc_bundle_hash seems unrelated to the race condition fix. If this is a separate bug fix, it should be mentioned in the PR description.

Security & Performance

No security concerns identified. Performance is improved through lazy initialization and singleton pattern.

Action Items Before Merge

  1. HIGH PRIORITY: Update async_props_emitter_spec.rb:23 to match new implementation
  2. MEDIUM PRIORITY: Add TypeScript tests for getOrCreateAsyncPropsManager()
  3. LOW PRIORITY: Check the test plan items and mark as completed

Overall Assessment

Approval Status: Approved with minor changes required

This is a well-thought-out fix for a real race condition. The implementation is clean and the documentation is excellent. The main concern is the test coverage gap - please update the RSpec test before merging.

Quality: 8/10
Test Coverage: 6/10 (needs update)
Documentation: 9/10
Security: 10/10

Great work on identifying and fixing this subtle race condition!

AbanoubGhadban and others added 6 commits January 14, 2026 14:04
This reverts commit 70cc5ef.
Previously, `onResponseStart(response)` was called with `void` (fire-and-forget),
allowing `handleIncrementalRenderStream` to complete before the HTTP response
was fully sent. This caused empty responses when all NDJSON data arrived at once
(e.g., when no `sleep` statements were present in async props blocks).

The fix captures the promise and awaits it at the end of the function, ensuring
the response stream is fully sent before the function returns.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@AbanoubGhadban AbanoubGhadban changed the title Fix async props race condition with lazy AsyncPropsManager initialization Fix async props race conditions Jan 15, 2026
@claude
Copy link

claude bot commented Jan 15, 2026

Code Review: Fix async props race condition

Summary

This PR successfully addresses a critical race condition (issue #2270) where async props fail to deliver when emit.call is invoked without artificial delays. The solution implements lazy initialization via getOrCreateAsyncPropsManager.

✅ What's Good

1. Excellent Design Pattern
The lazy initialization using getOrCreateAsyncPropsManager is the correct solution:

  • Thread-safe via sharedExecutionContext.get/set
  • Idempotent - multiple calls return same instance
  • No execution order assumptions

2. Comprehensive Implementation
All three layers updated correctly:

  • JavaScript: getOrCreateAsyncPropsManager in AsyncPropsManager.ts
  • Ruby: Update chunks call ReactOnRails.getOrCreateAsyncPropsManager
  • Integration: addAsyncPropsCapabilityToComponentProps uses new pattern

3. Clean API Changes
The signature change makes the API cleaner - manager access is now encapsulated

4. Good Documentation
Excellent comments explaining why lazy initialization is needed and how race conditions are handled

5. Debug Logs Removed
Good hygiene - all debug console.logs removed before final commit

🔍 Areas for Improvement

1. Missing Test for Race Condition (Medium Priority)
The existing tests don't test the actual race condition - they create manager first, then call getProp/setProp. Missing: Test where update chunks arrive BEFORE initial render.

Recommendation: Add test simulating update chunks arriving first.

2. Type Safety Enhancement (Low Priority)
ASYNC_PROPS_MANAGER_KEY is a string but could be a Symbol to prevent key collisions.

3. Documentation Enhancement (Low Priority)
Comments explain what sharedExecutionContext is but not who creates it or when it's cleared.

🔬 Code Quality

✅ Thread Safety: sharedExecutionContext scoped to single HTTP request
✅ Memory Leaks: None detected - manager cleared after response
✅ Performance: O(1) Map lookup, minimal overhead

📋 Pre-Merge Checklist (CLAUDE.md)

  • CRITICAL: Run bundle exec rubocop and fix all violations
  • CRITICAL: Ensure all files end with newline
  • Run tests: bundle exec rake run_rspec:gem
  • Run tests: bundle exec rake run_rspec:dummy
  • Test build: pnpm run build
  • Format check: pnpm run format.listDifferent

🎯 Final Verdict

APPROVE with minor suggestions

Solid fix for a real race condition. Core implementation is sound. Suggestions are for enhancing test coverage and documentation, not fixing critical issues.

Great work! 🚀

@claude
Copy link

claude bot commented Jan 15, 2026

Code Review: Fix async props race conditions

Summary

This PR addresses two critical race conditions in async props delivery for RSC (React Server Components) streaming. The fixes are well-thought-out and properly address the root causes described in issue #2270.


Strengths

1. Excellent Problem Analysis

  • Both race conditions are clearly identified and documented
  • The lazy initialization pattern is the correct solution for the AsyncPropsManager initialization race
  • Awaiting the response stream properly ensures data is fully sent before cleanup

2. Clean API Design

The getOrCreateAsyncPropsManager() function is well-designed:

  • Idempotent: Safe to call multiple times
  • Thread-safe within the Node.js event loop context
  • Well-documented: Clear JSDoc explains the race condition handling
  • Type-safe: Proper TypeScript typing with Map<string, unknown>

3. Consistent Application

The fix is applied consistently across all access points:

  • ReactOnRailsRSC.ts: Updated to use lazy initialization
  • async_props_emitter.rb: Update chunks now use getOrCreateAsyncPropsManager()
  • server_rendering_js_code.rb: Initial render setup simplified

4. Good Documentation

  • Comments explain the race condition scenarios clearly (lines 58-65 in server_rendering_js_code.rb)
  • JSDoc in AsyncPropsManager.ts explains the lazy initialization pattern
  • Type definitions updated to reflect the new signature

⚠️ Issues & Concerns

1. Critical: Potential Promise Handling Bug 🔴

In handleIncrementalRenderStream.ts:100:

onResponseStartPromise = Promise.resolve(onResponseStart(response));

Problem: Promise.resolve() wrapping a potentially async function can mask errors.

  • If onResponseStart() returns void (sync): This creates Promise<void>
  • If onResponseStart() returns Promise<void> (async): This creates Promise<Promise<void>> which resolves immediately to the inner promise, but errors in the inner promise might not propagate correctly

Better approach:

// Properly handle both sync and async cases
onResponseStartPromise = (async () => await onResponseStart(response))();

Or if you know it's always async:

onResponseStartPromise = onResponseStart(response);

Impact: If onResponseStart throws an error, it might not be caught properly.


2. Potential Null Pointer Issue ⚠️

In handleIncrementalRenderStream.ts:134:

await onResponseStartPromise;

Problem: onResponseStartPromise is initialized as null and only set if the first render request succeeds. If the stream ends without receiving any valid JSON objects, or if parsing the first object fails, this will await null, which is a no-op but semantically unclear.

Recommendation:

if (onResponseStartPromise) {
  await onResponseStartPromise;
}

Or initialize it to Promise.resolve() instead of null.


3. Error Logging Enhancement 💡

In async_props_emitter.rb:37-40, the error logging is improved but could be more actionable:

backtrace = e.backtrace&.first(5)&.join("\n")
"[ReactOnRailsPro::AsyncProps] FAILED to send prop '\#{prop_name}': " \
  "\#{e.class} - \#{e.message}\n\#{backtrace}"

Suggestion: Include the prop value (truncated/sanitized) to help debugging:

prop_preview = prop_value.inspect.truncate(100)
"[ReactOnRailsPro::AsyncProps] FAILED to send prop '\#{prop_name}' (value: \#{prop_preview}): ..."

4. Type Safety Consideration 📝

In async_props_emitter.rb:66, the generated JS code:

var asyncPropsManager = ReactOnRails.getOrCreateAsyncPropsManager(sharedExecutionContext);

Question: Is there validation that ReactOnRails exists and has this method? If the wrong bundle is loaded (non-RSC bundle), this could throw a cryptic error.

Recommendation: Add a safety check or ensure the error message is clear when this method is missing.


5. Performance Consideration 🚀

The node-renderer.js changes (lines 45-48) increase restart intervals significantly:

allWorkersRestartInterval: (env.CI ? 2 : env.RENDERER_ALL_WORKERS_RESTART_INTERVAL) || 10,
delayBetweenIndividualWorkerRestarts: env.CI ? 0.01 : 2,

Changes:

  • allWorkersRestartInterval: 0.2 minutes → 10 minutes (50x increase)
  • delayBetweenIndividualWorkerRestarts: 0.01 minutes → 2 minutes (200x increase)

Question: Are these changes related to the race condition fix, or are they unrelated performance tuning?

Recommendation: If unrelated, consider splitting into a separate commit with explanation. If related, document why the old aggressive restart intervals were problematic.


6. Testing Concerns 🧪

From the PR description:

  • Test stream_react_component_with_async_props without sleep delays

Questions:

  • Is there an automated test that verifies this race condition doesn't regress?
  • The dummy app test changes suggest manual testing, but is there a spec that covers this?

Recommendation: Add a regression test that:

  1. Calls emitter immediately without delays
  2. Verifies all async props are delivered
  3. Can be run in CI to prevent regression

🔍 Code Quality & Best Practices

Good:

  • ✅ Follows TypeScript best practices with proper typing
  • ✅ Error handling is comprehensive
  • ✅ Comments explain the "why" not just the "what"
  • ✅ Follows the project's pattern of using sharedExecutionContext for request-scoped state
  • ✅ RuboCop issue fixed (each_valueeach with explicit parameter)

Suggestions:

  • Consider extracting the constant 'asyncPropsManager' to a shared location if it's used in multiple files
  • The commit history shows some reverted commits and temp commits - these should be squashed before merge

🔐 Security Considerations

  • ✅ No security concerns identified
  • ✅ No user input is directly evaluated
  • ✅ JSON parsing errors are properly caught and handled
  • ✅ Error messages don't leak sensitive information

📊 Performance Impact

  • Positive: Lazy initialization avoids unnecessary AsyncPropsManager creation if not needed
  • Neutral: The getOrCreateAsyncPropsManager() lookup in Map is O(1)
  • Question: Worker restart interval changes (see concern Add Unit tests #5)

Recommended Actions Before Merge

  1. Fix the Promise.resolve() wrapping (Issue TODO for first version #1) - High priority
  2. Add null check for onResponseStartPromise (Issue Make work with turbolinks and better helper #2) - Medium priority
  3. Document or split out the worker restart interval changes (Issue Add Unit tests #5) - Medium priority
  4. Add regression test (Issue Detailed Setup and Usage Docs #6) - High priority for preventing future issues
  5. Squash temporary/reverted commits - Standard cleanup

🎯 Overall Assessment

Rating: ⭐⭐⭐⭐ (4/5)

This is a solid fix for a real race condition problem. The core approach (lazy initialization via getOrCreateAsyncPropsManager) is correct and well-executed. The main concerns are:

  • Promise handling edge case that could mask errors
  • Need for regression testing
  • Some unrelated changes that should be documented or separated

With the recommended fixes, this would be a 5/5 merge-ready PR.

Great work identifying and fixing these race conditions! 🚀

@claude
Copy link

claude bot commented Jan 15, 2026

Code Review - PR #2297: Fix async props race conditions

Summary

This PR effectively addresses two critical race conditions in the async props streaming implementation. The fixes are well-designed and properly documented. Overall, this is high-quality work with excellent attention to detail.


Strengths

  1. Excellent Problem Analysis: The PR clearly identifies and documents both race conditions with detailed comments explaining the solutions.

  2. Lazy Initialization Pattern: The getOrCreateAsyncPropsManager() function is a textbook example of proper lazy initialization to handle race conditions. The implementation is clean and thread-safe within the async/await model.

  3. Proper Promise Handling: Converting void onResponseStart() to await onResponseStartPromise correctly ensures the HTTP response completes before the function returns. This is crucial for preventing premature connection closures.

  4. Comprehensive Documentation: The inline comments, especially in server_rendering_js_code.rb (lines 61-67) and AsyncPropsManager.ts (lines 143-164), provide excellent context for future maintainers.

  5. Test Coverage: The integration tests properly verify the fixes work without artificial delays, which is the correct way to test race condition fixes.


🔍 Code Quality Observations

TypeScript/JavaScript Changes

packages/react-on-rails-pro-node-renderer/src/worker/handleIncrementalRenderStream.ts:40,100,134

  • ✅ Good: Proper null initialization of onResponseStartPromise
  • ✅ Good: Promise.resolve() wraps both sync and async return values correctly
  • ⚠️ Minor improvement opportunity: Consider adding a comment explaining why we await at line 134 AFTER onRequestEnded() (I assume it's to ensure the response starts before cleanup, but it's not immediately obvious)

packages/react-on-rails-pro/src/AsyncPropsManager.ts:152-164

  • Excellent: Clear function documentation with race condition explanation
  • Good: Proper TypeScript typing with Map<string, unknown>
  • Good: Idempotent behavior - safe to call multiple times

packages/react-on-rails-pro/src/ReactOnRailsRSC.ts:146

  • Critical fix: Passing sharedExecutionContext to addAsyncPropsCapabilityToComponentProps ensures the manager is accessible to update chunks
  • Good: Removed the return of asyncPropManager since it's no longer needed externally (it's stored in the context)

Ruby Changes

react_on_rails_pro/lib/react_on_rails_pro/async_props_emitter.rb:65,74

  • Critical fix: Using getOrCreateAsyncPropsManager() instead of directly accessing sharedExecutionContext.get() prevents nil errors when update chunks arrive first
  • Good: Enhanced error logging with backtrace (lines 37-39)

react_on_rails_pro/lib/react_on_rails_pro/request.rb:100

  • Question: Why change from bundle_timestamp to pool.rsc_bundle_hash? This seems unrelated to the race condition fix. Could you clarify the reason in a comment or commit message?

react_on_rails_pro/lib/react_on_rails_pro/server_rendering_js_code.rb:77-80

  • Good: Simplified JS code by removing manual sharedExecutionContext.set() since it's now handled internally
  • Good: Added race condition documentation (lines 61-67)

🐛 Potential Issues

Minor: Null Safety in handleIncrementalRenderStream.ts

Location: packages/react-on-rails-pro-node-renderer/src/worker/handleIncrementalRenderStream.ts:134

await onResponseStartPromise;

Issue: If the stream ends without processing any chunks (e.g., empty input), onResponseStartPromise remains null, and awaiting null is technically valid but semantically unclear.

Suggestion: Add a null check for clarity:

if (onResponseStartPromise) {
  await onResponseStartPromise;
}

Or initialize to Promise.resolve():

let onResponseStartPromise: Promise<void> = Promise.resolve();

🔒 Security Considerations

  • No security concerns: The changes maintain proper isolation via sharedExecutionContext scoping
  • Good: Error handling in AsyncPropsEmitter.call prevents one failed prop from crashing the entire render

Performance Considerations

  • No performance regressions: Lazy initialization adds negligible overhead
  • Good: Promise resolution is properly awaited, preventing resource leaks from unclosed connections

🧪 Test Coverage

react_on_rails_pro/spec/dummy/spec/requests/incremental_rendering_integration_spec.rb

  • Excellent: Tests verify bidirectional streaming without artificial delays (the real-world scenario)
  • Good: Timeout wrapper prevents tests from hanging indefinitely if race conditions still exist
  • ⚠️ Suggestion: Consider adding a specific test case for the "update chunk arrives before initial render" scenario to explicitly verify the lazy initialization fix

📋 Minor Suggestions

  1. Documentation: Consider adding a brief explanation in the PR description or code comment about why bundle_timestamppool.rsc_bundle_hash changed in request.rb:100

  2. Type Safety: The onResponseStartPromise null check mentioned above would improve type safety and readability

  3. Test Coverage: Add an explicit test for the update-chunk-arrives-first scenario if feasible


🎯 Conclusion

This is a well-crafted PR that solves legitimate race conditions with clean, maintainable code. The fixes are architecturally sound and properly documented.

Recommendation: ✅ Approve with minor suggestions

The minor issues mentioned above are not blockers - they're polish items that could be addressed now or in a follow-up PR. The core fixes are solid and ready to merge.

Great work on identifying and fixing these subtle timing issues! 🎉


Review conducted following CLAUDE.md guidelines | Generated with Claude Code

@claude
Copy link

claude bot commented Jan 15, 2026

Code Review - PR #2297: Fix async props race conditions

Summary

This PR addresses two critical race conditions in the async props streaming implementation. The fixes are well-architected and properly handle edge cases where update chunks arrive before or simultaneously with the initial render request.


Strengths

1. Excellent Problem Analysis

2. Clean Implementation

  • getOrCreateAsyncPropsManager() follows the singleton pattern correctly
  • Uses sharedExecutionContext as a request-scoped registry (proper isolation)
  • Maintains backward compatibility - existing code continues to work

3. Type Safety

  • Proper TypeScript types added to ReactOnRailsInternal interface
  • Function signatures are clear and well-documented
  • Return type changes from { asyncPropManager, props } to { props } makes sense (manager no longer needs to be returned since it's stored in context)

4. Documentation

  • Excellent inline documentation explaining the race condition scenario
  • JSDoc comments clearly describe the lazy initialization pattern
  • Comments explain WHY the change is needed, not just WHAT changed

🔍 Issues and Concerns

CRITICAL: Potential Race Condition in handleIncrementalRenderStream

Location: packages/react-on-rails-pro-node-renderer/src/worker/handleIncrementalRenderStream.ts:133-134

// Stream ended normally
await onRequestEnded();
await onResponseStartPromise;  // ⚠️ PROBLEM: Awaiting AFTER onRequestEnded

Issue: The fix for race condition #2 attempts to await the response stream completion, but the order is incorrect. By the time we await onResponseStartPromise, we've already called onRequestEnded(), which likely triggers endStream() on the AsyncPropsManager.

Expected behavior:

  1. Wait for HTTP response to finish sending all chunks (await onResponseStartPromise)
  2. THEN call onRequestEnded() to trigger endStream()

Current behavior:

  1. Call onRequestEnded() immediately
  2. Then wait for response (too late - endStream already called)

Recommendation: Swap the order:

// Stream ended normally - wait for response to complete FIRST
await onResponseStartPromise;
await onRequestEnded();

Impact: This could still cause async props to be rejected prematurely if endStream() is called before the last chunk is fully written to the HTTP response. The test might pass if timing is favorable, but the race condition may still exist in production.


MEDIUM: Test Coverage Gap

Location: Test files only include basic integration tests

Missing test scenarios:

  1. No explicit test for the race condition itself - The fix assumes commenting out sleep statements reproduces the bug, but there's no automated test that verifies:

    • Update chunks arriving BEFORE initial render
    • Update chunks arriving DURING initial render
    • Concurrent timing scenarios
  2. No test for onResponseStartPromise fix - No test verifies that the HTTP response completes before endStream() is called

Recommendation: Add specific race condition tests:

// Test that getOrCreateAsyncPropsManager handles order independence
it('handles update chunks arriving before initial render', async () => {
  // Simulate update chunk executing first
  const manager1 = getOrCreateAsyncPropsManager(context);
  manager1.setProp('test', 'value');
  
  // Simulate initial render executing second
  const manager2 = getOrCreateAsyncPropsManager(context);
  
  // Verify they're the same instance
  expect(manager1).toBe(manager2);
  
  // Verify prop was already set
  const result = await manager2.getProp('test');
  expect(result).toBe('value');
});

MINOR: Code Cleanup

Location: Multiple files have temporary logging that was later removed

Observation: The commit history shows debug logging was added then removed. This is good practice, but consider:

  • Adding permanent trace-level logging for production debugging (not console.log, but a proper logger)
  • Async props failures can be hard to debug in production without some visibility

Recommendation: Consider adding minimal structured logging at key points:

  • When AsyncPropsManager is created
  • When props are set/resolved
  • When endStream is called with unresolved props

Use a logger that can be configured to different levels (trace/debug/info/error) so it doesn't pollute production but helps with debugging.


MINOR: Naming Clarity

Location: packages/react-on-rails-pro/src/ReactOnRailsRSC.ts:145-158

Issue: The function addAsyncPropsCapabilityToComponentProps signature changed from:

// Before: Returns both manager and props
{ asyncPropManager, props }

// After: Returns only props (manager stored in context)
{ props }

This is a breaking change if any code was using the returned asyncPropManager. However, reviewing the codebase, it appears only internal code used this, and it's been updated.

Recommendation:

  • ✅ Change is fine for internal Pro package
  • ⚠️ If this is part of a public API, document as breaking change in CHANGELOG_PRO.md
  • Consider whether the function name should change to reflect it no longer returns the manager

🧪 Testing Recommendations

  1. Add unit tests for getOrCreateAsyncPropsManager:

    • Test creating new manager
    • Test retrieving existing manager
    • Test that multiple calls return same instance
    • Test with different context objects (isolation)
  2. Add integration test for onResponseStartPromise fix:

    • Verify endStream is called AFTER HTTP response completes
    • Verify last async prop is delivered before stream ends
  3. Add stress test:

    • Send many async props rapidly
    • Verify all are delivered correctly
    • Verify no props are lost or delivered out of order

📋 Pre-Merge Checklist

  • Fix the order of await onResponseStartPromise and await onRequestEnded()
  • Add unit tests for getOrCreateAsyncPropsManager
  • Add integration test that explicitly tests the race condition
  • Update CHANGELOG_PRO.md if addAsyncPropsCapabilityToComponentProps signature change is public API
  • Consider adding trace-level logging for production debugging
  • Verify CI passes (currently in progress)

🎯 Overall Assessment

Quality: Good - The PR correctly identifies and addresses real race conditions with clean architecture.

Readiness: Needs revision - The critical issue with onResponseStartPromise order should be fixed before merge.

Risk Level: Medium - The lazy initialization fix is solid, but the stream completion fix may not fully resolve race condition #2.


💡 Additional Notes

The PR demonstrates excellent understanding of the async rendering flow and proper use of execution context for request-scoped state. The documentation and code comments are exemplary. Once the order issue in handleIncrementalRenderStream is addressed and tests are enhanced, this will be a solid fix.

Great work on tackling a complex concurrency issue! 🚀

@claude
Copy link

claude bot commented Jan 15, 2026

Code Review: Fix async props race conditions

Overview

This PR addresses two critical race conditions in the async props streaming implementation. The fixes are well-designed and properly handle timing issues between initial render and update chunks.


✅ Strengths

1. Excellent Race Condition Analysis

The PR clearly identifies and fixes two distinct race conditions:

  • Race 1: AsyncPropsManager initialization timing (update chunks arriving before initial render)
  • Race 2: Response stream completion timing (function returning before HTTP response sent)

2. Clean Lazy Initialization Pattern

The getOrCreateAsyncPropsManager() function is well-implemented with clear documentation explaining the race condition, proper lazy initialization pattern, and consistent key usage.

3. Proper Promise Handling

The fix in handleIncrementalRenderStream.ts correctly captures and awaits the response promise, ensuring the HTTP response is fully sent before the function returns.

4. Comprehensive API Updates

  • Updated TypeScript types consistently across all files
  • Added function to both RSC bundle and Pro client stub
  • Updated Ruby side to use new lazy initialization
  • Removed unnecessary is_rsc_payload parameter (good simplification)

5. Improved Error Logging

Better error context in async_props_emitter.rb with backtrace information


🔍 Code Quality Observations

✅ Consistent pattern usage across Ruby and TypeScript sides
✅ Good test coverage with integration tests verifying fix without sleep delays
✅ Clean refactoring that simplifies the code


⚠️ Potential Concerns (Minor)

  1. Null Promise Handling: In handleIncrementalRenderStream.ts:134, onResponseStartPromise could be null if stream ends before first object. Consider adding a comment explaining this is intentional.

  2. Type Safety: The cast in AsyncPropsManager.ts:155 assumes the stored value is an AsyncPropsManager. Runtime validation could be added in debug mode, but probably overkill for internal code.

  3. Test File Changes: The new test helper functions (addStreamValueToFirstBundle, endFirstBundleStream) could use documentation comments explaining their purpose.


🛡️ Security Considerations

✅ No security concerns identified


🚀 Performance Considerations

✅ Positive performance impact - reduced latency, proper streaming, minimal overhead


📋 Test Coverage Assessment

✅ Good coverage. Recommendation: Consider adding a specific test case that explicitly tests the race condition scenario.


🎯 Breaking Changes Assessment

⚠️ API Changes (Internal only) - No breaking changes for end users


✅ Final Recommendation

APPROVE with minor suggestions

This is a well-crafted fix for a real timing issue. The code is clean, well-documented, and properly tested. The lazy initialization pattern is the right solution for this race condition.

Suggested before merge (all optional):

  1. Add comment to explain null promise handling
  2. Consider runtime type validation in debug mode
  3. Add explicit race condition test case

No blocking issues identified. Ready to merge as-is.


🤖 Review Checklist

  • ✅ Code quality and best practices - Excellent
  • ✅ Potential bugs or issues - None identified
  • ✅ Performance considerations - Positive impact
  • ✅ Security concerns - None identified
  • ✅ Test coverage - Good, with suggestions for improvement

Great work on identifying and fixing these race conditions! 🎉

Add comprehensive tests for the lazy initialization factory function
that handles race conditions between initial render and update chunks:

- Update chunks arriving BEFORE initial render (setProp then getProp)
- Update chunks arriving DURING initial render (interleaved operations)
- Update chunks arriving AFTER initial render (getProp then setProp)
- Multiple props set before any getProp
- endStream called from update chunk context
- Manager isolation between different execution contexts

These tests verify that getOrCreateAsyncPropsManager returns the same
AsyncPropsManager instance regardless of call order, ensuring props set
by update chunks are visible to the initial render and vice versa.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@claude
Copy link

claude bot commented Jan 15, 2026

Code Review for PR #2297: Fix async props race conditions

Summary

This PR fixes two critical race conditions in the async props streaming implementation. The fixes are well-thought-out and properly address the root causes.


✅ Strengths

  1. Excellent Problem Analysis: The PR correctly identifies both race conditions:

    • Update chunks arriving before AsyncPropsManager initialization
    • Response stream ending before HTTP response completion
  2. Strong Test Coverage: The new tests in AsyncPropManager.test.ts (lines 152-273) are comprehensive:

    • Tests lazy initialization with multiple call scenarios
    • Covers all timing permutations (before/during/after initial render)
    • Tests isolation between execution contexts
    • Tests concurrent scenarios with endStream
  3. Good Architectural Pattern: The getOrCreateAsyncPropsManager() factory function is a clean solution:

    • Encapsulates lazy initialization logic
    • Provides single point of access
    • Properly scoped to shared execution context
  4. Backward Compatible: Changes maintain the existing API while fixing the race conditions

  5. Documentation: Excellent inline documentation explaining the race condition handling


🔍 Code Quality Observations

JavaScript/TypeScript Side

handleIncrementalRenderStream.ts (lines 40, 100, 134)

let onResponseStartPromise: Promise<void> | null = null;
// ...
onResponseStartPromise = Promise.resolve(onResponseStart(response));
// ...
await onResponseStartPromise;

Good: Properly captures and awaits the promise
⚠️ Potential Issue: If onResponseStart() is never called (e.g., early return or exception), onResponseStartPromise stays null and line 134 attempts to await null. While this works in JavaScript (awaiting null/undefined is a no-op), it could be made more explicit:

if (onResponseStartPromise) {
  await onResponseStartPromise;
}

AsyncPropsManager.ts (lines 152-164)
Excellent: Clean factory pattern with proper type safety
Good: Uses a constant for the key to avoid typos

Ruby Side

async_props_emitter.rb (lines 37-39)
Improved error logging: Adding backtrace is helpful for debugging

server_rendering_js_code.rb (lines 79)

var { props: propsWithAsyncProps } = ReactOnRails.addAsyncPropsCapabilityToComponentProps(usedProps, sharedExecutionContext);

Good: Now passes sharedExecutionContext to enable lazy initialization
Simplified: Removed the now-unnecessary asyncPropManager destructuring and manual storage

request.rb (line 99)

emitter = ReactOnRailsPro::AsyncPropsEmitter.new(pool.rsc_bundle_hash, request)

Simplified: Always uses rsc_bundle_hash since async props only work with RSC bundles


🎯 Performance Considerations

  1. No Performance Regression: The lazy initialization adds minimal overhead (single Map lookup)
  2. Promise Handling: Using Promise.resolve() wrapper (line 100 of handleIncrementalRenderStream.ts) handles both sync and async returns correctly
  3. Removed Sleep Delays: Good that test now works without artificial delays (test_incremental_rendering.html.erb)

🔒 Security Considerations

No security issues identified

  • Proper error handling prevents information leakage
  • Execution context isolation maintained
  • No new injection vectors introduced

📋 Test Coverage Assessment

New Tests Added:

  • ✅ 8 new test cases in AsyncPropManager.test.ts
  • ✅ Covers lazy initialization thoroughly
  • ✅ Tests race condition scenarios

Updated Tests:

  • ✅ All existing specs updated to reflect API changes
  • ✅ Integration spec removes sleep delays (proving fix works)

Missing Tests:
⚠️ Minor: No test for the onResponseStartPromise = null edge case in handleIncrementalRenderStream.ts


🐛 Potential Issues

  1. Minor - Null Safety: As mentioned above, awaiting onResponseStartPromise when it's null works but could be more explicit

  2. Minor - Error Context: In async_props_emitter.rb line 38, limiting backtrace to 5 lines might hide useful context in deeply nested calls. Consider making this configurable or increasing to 10.

  3. Documentation - API Change: The addAsyncPropsCapabilityToComponentProps function signature changed (now requires sharedExecutionContext). This is a breaking change for any code calling it directly. However, looking at the codebase, this appears to be internal-only, so it's acceptable.


📝 Suggestions

  1. Consider adding null check:
// In handleIncrementalRenderStream.ts, line 133-134
await onRequestEnded();
if (onResponseStartPromise) {
  await onResponseStartPromise;
}
  1. Consider adding JSDoc to the factory function (already has good inline docs, but could add @example)

  2. Worker restart intervals: The node-renderer.js changes (lines 45-48) increase restart intervals from 0.2 to 10 minutes. This seems unrelated to the race condition fix. Consider:

    • Splitting into separate commit/PR for clarity
    • Adding comment explaining why this change was needed
    • Or reverting if it was accidental

✅ Recommended Actions Before Merge

  1. Optional: Add null check for onResponseStartPromise before awaiting
  2. Clarify: Is the worker restart interval change (node-renderer.js) intentional? If so, add a comment explaining why
  3. Verify: Ensure all CI checks pass (currently queued)

🎉 Overall Assessment

Rating: Excellent ⭐⭐⭐⭐⭐

This is a high-quality PR that properly fixes subtle race conditions with:

  • ✅ Correct root cause analysis
  • ✅ Clean architectural solution
  • ✅ Comprehensive test coverage
  • ✅ Good documentation
  • ✅ No breaking changes to public APIs
  • ✅ Backward compatible

The issues mentioned above are minor and don't block merging. Great work! 🚀


Generated by Claude Code Review

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