Skip to content

Commit db48f68

Browse files
Add AsyncPropsEmitter for incremental rendering
Implement AsyncPropsEmitter class to support sending async props incrementally during streaming render. This is the first component for Ruby-side incremental rendering support. Features: - call(prop_name, value) API for emitting async props - Generates NDJSON update chunks with bundleTimestamp and updateChunk - Creates JavaScript code to call asyncPropsManager.setProp() - Error handling: logs errors but continues (doesn't abort render) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 75d26e6 commit db48f68

File tree

2 files changed

+85
-0
lines changed

2 files changed

+85
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
module ReactOnRailsPro
4+
# Emitter class for sending async props incrementally during streaming render
5+
# Used by stream_react_component_with_async_props helper
6+
class AsyncPropsEmitter
7+
def initialize(bundle_timestamp, request_stream)
8+
@bundle_timestamp = bundle_timestamp
9+
@request_stream = request_stream
10+
end
11+
12+
# Public API: emit.call('propName', propValue)
13+
# Sends an update chunk to the node renderer to resolve an async prop
14+
def call(prop_name, prop_value)
15+
update_chunk = generate_update_chunk(prop_name, prop_value)
16+
@request_stream.write("#{update_chunk.to_json}\n")
17+
rescue StandardError => e
18+
Rails.logger.error do
19+
"[ReactOnRailsPro] Failed to send async prop '#{prop_name}': #{e.message}"
20+
end
21+
# Continue - don't abort entire render because one prop failed
22+
end
23+
24+
private
25+
26+
def generate_update_chunk(prop_name, value)
27+
{
28+
bundleTimestamp: @bundle_timestamp,
29+
updateChunk: generate_set_prop_js(prop_name, value)
30+
}
31+
end
32+
33+
def generate_set_prop_js(prop_name, value)
34+
<<~JS.strip
35+
(function(){
36+
var asyncPropsManager = sharedExecutionContext.get("asyncPropsManager");
37+
asyncPropsManager.setProp(#{prop_name.to_json}, #{value.to_json});
38+
})()
39+
JS
40+
end
41+
end
42+
end
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "spec_helper"
4+
require "react_on_rails_pro/async_props_emitter"
5+
6+
RSpec.describe ReactOnRailsPro::AsyncPropsEmitter do
7+
let(:bundle_timestamp) { "bundle-12345" }
8+
# rubocop:disable RSpec/VerifiedDoubleReference
9+
let(:request_stream) { instance_double("RequestStream") }
10+
# rubocop:enable RSpec/VerifiedDoubleReference
11+
let(:emitter) { described_class.new(bundle_timestamp, request_stream) }
12+
13+
describe "#call" do
14+
it "writes NDJSON update chunk with correct structure" do
15+
allow(request_stream).to receive(:write)
16+
17+
emitter.call("books", ["Book 1", "Book 2"])
18+
19+
expect(request_stream).to have_received(:write) do |output|
20+
expect(output).to end_with("\n")
21+
parsed = JSON.parse(output.chomp)
22+
expect(parsed["bundleTimestamp"]).to eq(bundle_timestamp)
23+
expect(parsed["updateChunk"]).to include('sharedExecutionContext.get("asyncPropsManager")')
24+
expect(parsed["updateChunk"]).to include('asyncPropsManager.setProp("books", ["Book 1","Book 2"])')
25+
end
26+
end
27+
28+
it "logs error and continues without raising when write fails" do
29+
mock_logger = instance_double(Logger)
30+
allow(Rails).to receive(:logger).and_return(mock_logger)
31+
allow(request_stream).to receive(:write).and_raise(StandardError.new("Connection lost"))
32+
allow(mock_logger).to receive(:error)
33+
34+
expect { emitter.call("books", []) }.not_to raise_error
35+
36+
expect(mock_logger).to have_received(:error) do |&block|
37+
message = block.call
38+
expect(message).to include("Failed to send async prop 'books'")
39+
expect(message).to include("Connection lost")
40+
end
41+
end
42+
end
43+
end

0 commit comments

Comments
 (0)