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"
CLOSED_REQUEST_STREAM_SOCKET_ERRORS =

Socket-level errors that mean the renderer request stream is already gone. These mirror the family that ReactOnRailsPro::Stream#log_client_disconnect (concerns/stream.rb) treats as a routine client disconnect, so the two files classify disconnect races the same way.

[
  IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of AsyncPropsEmitter.



63
64
65
66
67
68
69
70
71
72
73
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 63

def initialize(bundle_timestamp, request_stream, pull_enabled: false)
  @bundle_timestamp = bundle_timestamp
  @request_stream = request_stream
  @pushed_props = Set.new
  # Latched the first time a write hits a closed request stream, so the rest of
  # the user's block (which runs in its own fiber and may keep emitting) is
  # skipped silently instead of logging once per remaining prop.
  @request_stream_closed = false
  @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.



61
62
63
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 61

def pull_requests
  @pull_requests
end

Class Method Details

.closed_request_stream_errorsObject

Exception classes that indicate a write landed on an already-closed request stream. @request_stream is a Protocol::HTTP::Body::Writable::Output; writing after its queue closes raises Protocol::HTTP::Body::Writable::Closed on a clean close, or the stored socket error (Errno::*/IOError) that tore the stream down on an aborted one (see async-http Output#passthrough -> Writable#write).

Resolved at rescue time rather than at load time on purpose: the protocol-http writable-body class is required lazily (only once streaming starts) and its Closed constant has moved namespaces across gem versions, so the defined? guard keeps this file loadable everywhere and picks the class up once it exists.



85
86
87
88
89
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 85

def self.closed_request_stream_errors
  return CLOSED_REQUEST_STREAM_SOCKET_ERRORS unless defined?(Protocol::HTTP::Body::Writable::Closed)

  CLOSED_REQUEST_STREAM_SOCKET_ERRORS + [Protocol::HTTP::Body::Writable::Closed]
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).



94
95
96
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 94

def call(prop_name, prop_value)
  write_settled_chunk(prop_name, action: "send") { generate_update_chunk(prop_name, prop_value) }
end

#end_stream_chunkObject

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



107
108
109
110
111
112
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 107

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

#reject(prop_name, reason) ⇒ Object

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



99
100
101
102
103
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 99

def reject(prop_name, reason)
  # 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.
  write_settled_chunk(prop_name, action: "reject") { generate_reject_chunk(prop_name, reason) }
end

#render_complete!Object

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



116
117
118
# File 'lib/react_on_rails_pro/async_props_emitter.rb', line 116

def render_complete!
  @pull_requests&.close
end