Class: ReactOnRailsPro::AsyncPropsEmitter

Inherits:
Object
  • Object
show all
Defined in:
lib/react_on_rails_pro/async_props_emitter.rb

Overview

Emitter class for sending async props incrementally during streaming render. Used by stream_react_component_with_async_props helper.

PROTOCOL: Each call to ‘emit.call(prop_name, value)` sends an NDJSON line to the Node renderer:

{"bundleTimestamp": "abc123", "updateChunk": "(function(){...})()"}

The updateChunk JavaScript accesses the AsyncPropsManager via sharedExecutionContext and resolves the promise for that prop, allowing React to continue rendering.

WHY NOT USE GLOBAL VARIABLES? Global variables in Node.js VM persist across requests, causing data leakage. sharedExecutionContext is scoped to a single HTTP request (ExecutionContext).

PULL MODE: When pull_enabled is true, React components can request props lazily via getProp(). Those requests arrive as propRequest chunks on the response stream. ‘pull_requests` exposes an Async::Queue that yields prop names as they arrive. The user’s block can dequeue and resolve them dynamically.

Examples:

Push-only usage (existing)

stream_react_component_with_async_props("Dashboard") do |emit|
  emit.call("users", User.all.to_a)
  emit.call("posts", Post.recent.to_a)
end

Pull mode usage

stream_react_component_with_async_props("Dashboard", push_props: %w[stats]) do |emit|
  emit.call("stats", compute_stats)
  while (prop_name = emit.pull_requests.dequeue)
    emit.call(prop_name, fetch_prop(prop_name))
  end
end

Constant Summary collapse

SANITIZED_REJECTION_REASON =
"Async prop rejected by server"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bundle_timestamp, request_stream, pull_enabled: false) ⇒ AsyncPropsEmitter

Returns a new instance of AsyncPropsEmitter.



55
56
57
58
59
60
61
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 55

def initialize(bundle_timestamp, request_stream, pull_enabled: false)
  @bundle_timestamp = bundle_timestamp
  @request_stream = request_stream
  @pushed_props = Set.new
  @pull_enabled = pull_enabled
  @pull_requests = PullRequestQueue.new(@pushed_props) if pull_enabled
end

Instance Attribute Details

#pull_requestsObject (readonly)

Returns the value of attribute pull_requests.



53
54
55
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 53

def pull_requests
  @pull_requests
end

Instance Method Details

#call(prop_name, prop_value) ⇒ Object

Sends an async prop to the Node renderer. The prop value is JSON-serialized and sent as an NDJSON line. On the Node side, this triggers asyncPropsManager.setProp(propName, value).



70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 70

def call(prop_name, prop_value)
  update_chunk = generate_update_chunk(prop_name, prop_value)
  @request_stream << "#{update_chunk.to_json}\n"
  @pushed_props.add(prop_name)
rescue StandardError => e
  # Continue streaming: one failed async prop write should not abort the
  # entire render. The prop is not marked as pushed unless the write
  # succeeds, so pull mode can request it again instead of silently hanging.
  Rails.logger.error do
    backtrace = e.backtrace&.first(5)&.join("\n")
    "[ReactOnRailsPro::AsyncProps] Failed to send async prop '#{prop_name}': " \
      "#{e.class} - #{e.message}\n#{backtrace}"
  end
end

#end_stream_chunkObject

Generates the chunk that should be executed when the request stream closes. This tells the asyncPropsManager to end the stream.



102
103
104
105
106
107
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 102

def end_stream_chunk
  {
    bundleTimestamp: @bundle_timestamp,
    updateChunk: generate_end_stream_js
  }
end

#pull_enabled?Boolean

Returns:

  • (Boolean)


63
64
65
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 63

def pull_enabled?
  @pull_enabled
end

#reject(prop_name, reason) ⇒ Object

Rejects an async prop on the Node side so React can show an error boundary.



86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 86

def reject(prop_name, reason)
  update_chunk = generate_reject_chunk(prop_name, reason)
  @request_stream << "#{update_chunk.to_json}\n"
  # Once the reject chunk is written, Ruby treats the prop as settled too.
  # That keeps duplicate pull requests filtered even if the JS manager is recreated.
  @pushed_props.add(prop_name)
rescue StandardError => e
  Rails.logger.error do
    backtrace = e.backtrace&.first(5)&.join("\n")
    "[ReactOnRailsPro::AsyncProps] Failed to reject async prop '#{prop_name}': " \
      "#{e.class} - #{e.message}\n#{backtrace}"
  end
end

#render_complete!Object

Called by stream_request when the response stream signals render complete. Closes the pull_requests queue so dequeue returns nil.



111
112
113
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 111

def render_complete!
  @pull_requests&.close
end