Class: ReactOnRailsPro::Request

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

Overview

rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: UploadAssetsWaiter

Constant Summary collapse

CONNECTION_MUTEX =

Mutex for thread-safe connection management. Using a constant eliminates the race condition that would exist with @mutex ||= Mutex.new

Mutex.new
UPLOAD_ASSETS_MUTEX =
Mutex.new
UPLOAD_ASSET_FINGERPRINT_MUTEX =
Mutex.new

Class Method Summary collapse

Class Method Details

.asset_exists_on_vm_renderer?(filename) ⇒ Boolean

Returns:

  • (Boolean)


241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/react_on_rails_pro/request.rb', line 241

def asset_exists_on_vm_renderer?(filename)
  Rails.logger.info { "[ReactOnRailsPro] Sending request to check if file exist on node-renderer: #{filename}" }

  form_data = common_form_data

  # Add targetBundles from the current bundle hash and RSC bundle hash
  pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
  target_bundles = [pool.server_bundle_hash]

  target_bundles << pool.rsc_bundle_hash if ReactOnRailsPro.configuration.enable_rsc_support

  form_data["targetBundles"] = target_bundles

  response = perform_request("/asset-exists?filename=#{filename}", json: form_data)
  JSON.parse(response.body)["exists"] == true
end

.render_code(path, js_code, send_bundle) ⇒ Object



89
90
91
92
93
# File 'lib/react_on_rails_pro/request.rb', line 89

def render_code(path, js_code, send_bundle)
  Rails.logger.info { "[ReactOnRailsPro] Perform rendering request #{path}" }
  form = form_with_code(js_code, send_bundle)
  perform_request(path, form:)
end

.render_code_as_stream(path, js_code, is_rsc_payload:, rsc_stream_observability: false) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/react_on_rails_pro/request.rb', line 95

def render_code_as_stream(path, js_code, is_rsc_payload:, rsc_stream_observability: false)
  Rails.logger.info { "[ReactOnRailsPro] Perform rendering request as a stream #{path}" }
  if is_rsc_payload && !ReactOnRailsPro.configuration.enable_rsc_support
    raise ReactOnRailsPro::Error,
          "RSC support is not enabled. Please set enable_rsc_support to true in your " \
          "config/initializers/react_on_rails_pro.rb file before " \
          "rendering any RSC payload."
  end

  warn_cb = ->(request_time) { warn_if_slow_streaming_first_chunk(path, request_time) }
  ReactOnRailsPro::StreamRequest.create(first_chunk_warn_callback: warn_cb) do |send_bundle, _tasks|
    if send_bundle
      Rails.logger.info { "[ReactOnRailsPro] Sending bundle to the node renderer" }
      upload_assets
    end

    form = form_with_code(js_code, false, rsc_stream_observability:)
    perform_request(path, form:, stream: true)
  end
end

.render_code_with_incremental_updates(path, js_code, async_props_block:, pull_enabled: false, push_props: nil, rsc_stream_observability: false) ⇒ Object

Performs an incremental render request with bidirectional HTTP/2 streaming.

ARCHITECTURE: This method orchestrates the async props flow:

┌─────────────────────────────────────────────────────────────────────────┐ │ Rails Thread (main) │ Rails Thread (async task) │ ├───────────────────────────────────┼─────────────────────────────────────┤ │ 1. Send initial NDJSON line │ │ │ ... │ │ │ │ │ │ 2. Return response stream │ 3. Execute async_props_block │ │ (caller processes HTML) │ emit.call("users", User.all) │ │ │ └── Sends NDJSON: updateChunk │ │ │ emit.call("posts", Post.all) │ │ │ └── Sends NDJSON: updateChunk │ │ │ │ │ ... streaming HTML chunks ... │ 4. Block completes │ │ │ output.close (sends END_STREAM) │ └───────────────────────────────────┴─────────────────────────────────────┘

WHY async task?

  • We need to return the response stream immediately so Rails can start sending HTML
  • The async_props_block runs concurrently, sending props as they become available
  • When the block finishes, we close the output (END_STREAM flag)
  • Node's handleRequestClosed then calls asyncPropsManager.endStream()


142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/react_on_rails_pro/request.rb', line 142

def render_code_with_incremental_updates(
  path,
  js_code,
  async_props_block:,
  pull_enabled: false,
  push_props: nil,
  rsc_stream_observability: false
)
  Rails.logger.info { "[ReactOnRailsPro] Perform incremental rendering request #{path}" }

  pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
  if !push_props.nil? && !pull_enabled
    raise ArgumentError, "push_props can only be provided when pull_enabled is true"
  end

  warn_cb = ->(request_time) { warn_if_slow_streaming_first_chunk(path, request_time) }
  ReactOnRailsPro::StreamRequest.create(
    first_chunk_warn_callback: warn_cb,
    pull_enabled:
  ) do |send_bundle, tasks|
    if send_bundle
      Rails.logger.info { "[ReactOnRailsPro] Sending bundle to the node renderer" }
      upload_assets
    end

    # Open a bidirectional HTTP/2 stream using async-http's Writable body.
    # output supports << (alias for write) and close (sends END_STREAM).
    output, response = connection.post_bidi(
      path,
      headers: [["content-type", "application/x-ndjson"]]
    )

    emitter = ReactOnRailsPro::AsyncPropsEmitter.new(
      pool.rsc_bundle_hash,
      output,
      pull_enabled:
    )
    initial_data = build_initial_incremental_request(
      js_code, emitter, pull_enabled:, push_props:, rsc_stream_observability:
    )

    # Send the initial render request as first NDJSON line
    output << "#{initial_data.to_json}\n"

    # Execute async props block in a separate fiber.
    # This runs concurrently with the response streaming back to the client.
    tasks.push(Async::Task.current.async do
      async_props_block.call(emitter)
    ensure
      # When the block completes (or raises), close the output.
      # This sends HTTP/2 END_STREAM flag, triggering Node's handleRequestClosed.
      output.close
    end)

    { pull_result: true, response:, emitter: pull_enabled ? emitter : nil }
  end
end

.reset_connectionObject



79
80
81
82
83
84
85
86
87
# File 'lib/react_on_rails_pro/request.rb', line 79

def reset_connection
  CONNECTION_MUTEX.synchronize do
    new_conn = create_connection
    old_conn = @connection
    ReactOnRailsPro::RendererHttpClient.bump_client_generation
    @connection = new_conn
    old_conn&.close
  end
end

.upload_assetsObject



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/react_on_rails_pro/request.rb', line 200

def upload_assets
  Rails.logger.info { "[ReactOnRailsPro] Uploading assets" }

  # Early checks with descriptive messages. add_bundle_to_form(check_bundle: true) also
  # validates existence, but these provide clearer context for the rake task user.
  server_bundle_path = ReactOnRails::Utils.server_bundle_js_file_path
  unless File.exist?(server_bundle_path)
    raise ReactOnRailsPro::Error, "Server bundle not found at #{server_bundle_path}. " \
                                  "Please build your bundles before uploading assets."
  end

  # Create a list of bundle timestamps to send to the node renderer
  pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
  target_bundles = [pool.server_bundle_hash]

  # Add RSC bundle if enabled
  if ReactOnRailsPro.configuration.enable_rsc_support
    rsc_bundle_path = ReactOnRailsPro::Utils.rsc_bundle_js_file_path
    unless File.exist?(rsc_bundle_path)
      raise ReactOnRailsPro::Error, "RSC bundle not found at #{rsc_bundle_path}. " \
                                    "Please build your bundles before uploading assets."
    end
    target_bundles << pool.rsc_bundle_hash
  end

  assets_to_copy = assets_to_copy_for_upload

  with_asset_upload_single_flight(upload_assets_single_flight_key(target_bundles, assets_to_copy)) do
    form = form_with_assets_and_bundle(assets_to_copy:)
    # TODO: targetBundles is only kept for backward compatibility with older node renderers
    # (protocol 2.0.0) that require it. The new node renderer derives target directories from
    # the bundle_<hash> form keys and ignores this field. Remove at the next breaking version.
    # Note: it's not mandatory to keep this until then — users are expected to upgrade the
    # node renderer and react_on_rails gem to the same version together — but it's an easy
    # backward compatibility safeguard.
    form["targetBundles"] = target_bundles

    perform_request("/upload-assets", form:)
  end
end