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

RAW_RENDER_CONTENT_TYPE =
"application/vnd.react-on-rails.render-request+javascript"
RAW_RENDER_HEADER_PREFIX =
"x-react-on-rails-pro-"
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)


227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/react_on_rails_pro/request.rb', line 227

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

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

.render_code(path, js_code, send_bundle, bundle_role: :server, artifacts: nil) ⇒ Object



93
94
95
96
97
98
99
# File 'lib/react_on_rails_pro/request.rb', line 93

def render_code(path, js_code, send_bundle, bundle_role: :server, artifacts: nil)
  Rails.logger.info { "[ReactOnRailsPro] Perform rendering request #{path}" }
  artifacts = resolve_upload_artifacts(artifacts, action_description: "uploading requested assets") if send_bundle
  request_path = send_bundle ? retarget_render_path(path, artifacts, bundle_role) : path
  form = form_with_code(js_code, send_bundle, artifacts:)
  perform_render_request(request_path, js_code, form:, send_bundle:)
end

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



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/react_on_rails_pro/request.rb', line 101

def render_code_as_stream(path, js_code, is_rsc_payload:, rsc_stream_observability: false, artifacts: nil)
  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) }
  bundle_role = is_rsc_payload ? :rsc : :server
  ReactOnRailsPro::StreamRequest.create(first_chunk_warn_callback: warn_cb) do |send_bundle, _tasks|
    request_path, request_artifacts = prepare_streaming_request(path, send_bundle:, bundle_role:, artifacts:)

    form = form_with_code(js_code, false, artifacts: request_artifacts, rsc_stream_observability:)
    perform_render_request(request_path, js_code, form:, send_bundle: false, stream: true)
  end
end

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

Performs an incremental render request with bidirectional 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 (request EOF) │ └───────────────────────────────────┴─────────────────────────────────────┘

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 request body
  • Node's handleRequestClosed then calls asyncPropsManager.endStream()


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
199
200
201
202
203
# File 'lib/react_on_rails_pro/request.rb', line 146

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

  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) }
  bundle_role = is_rsc_payload ? :rsc : :server
  ReactOnRailsPro::StreamRequest.create(
    first_chunk_warn_callback: warn_cb,
    pull_enabled:
  ) do |send_bundle, tasks|
    request_path, request_artifacts = prepare_streaming_request(path, send_bundle:, bundle_role:, artifacts:)

    # Open a bidirectional request using async-http's Writable body.
    # output supports << (alias for write) and close (signals request EOF).
    output, response = connection.post_bidi(
      request_path,
      headers: [["content-type", "application/x-ndjson"]]
    )

    emitter = build_incremental_emitter(request_artifacts, output, pull_enabled:)
    write_initial_incremental_request(
      output,
      js_code,
      emitter,
      artifacts: request_artifacts,
      pull_enabled:,
      push_props:,
      rsc_stream_observability:
    )

    # Execute async props block in a separate fiber.
    # This runs concurrently with the response streaming back to the client.
    parent_context = ReactOnRailsPro::OpenTelemetry.capture_context
    tasks.push(Async::Task.current.async do
      ReactOnRailsPro::OpenTelemetry.with_context(parent_context) do
        async_props_block.call(emitter)
      end
    ensure
      # When the block completes (or raises), close the output.
      # This signals request-body EOF, triggering Node's handleRequestClosed.
      output.close
    end)

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

.reset_connectionObject



83
84
85
86
87
88
89
90
91
# File 'lib/react_on_rails_pro/request.rb', line 83

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_assets(artifacts: nil) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/react_on_rails_pro/request.rb', line 205

def upload_assets(artifacts: nil)
  Rails.logger.info { "[ReactOnRailsPro] Uploading assets" }
  artifacts = resolve_upload_artifacts(artifacts, action_description: "uploading assets")
  target_bundles = artifacts.map(&:id)

  # Artifact IDs already bind every bundle and companion byte. Keying the
  # single-flight upload by those IDs avoids re-reading live paths after
  # the operation-scoped snapshot was constructed.
  with_asset_upload_single_flight(upload_assets_single_flight_key(target_bundles, [])) do
    form = form_with_assets_and_bundle(artifacts:)
    # 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