Class: ReactOnRailsPro::Request

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

Overview

rubocop:disable Metrics/ClassLength

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

Class Method Summary collapse

Class Method Details

.asset_exists_on_vm_renderer?(filename) ⇒ Boolean

Returns:

  • (Boolean)


155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/react_on_rails_pro/request.rb', line 155

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



24
25
26
27
28
# File 'lib/react_on_rails_pro/request.rb', line 24

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: form)
end

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



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/react_on_rails_pro/request.rb', line 30

def render_code_as_stream(path, js_code, is_rsc_payload:)
  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)
    perform_request(path, form: form, stream: true)
  end
end

.render_code_with_incremental_updates(path, js_code, async_props_block:) ⇒ 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()



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/react_on_rails_pro/request.rb', line 77

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

  pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool

  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

    # 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"]]
    )

    # Create emitter — output has the same interface as the old HTTPX request
    # object (<< for writing, close for END_STREAM), so AsyncPropsEmitter works unchanged.
    emitter = ReactOnRailsPro::AsyncPropsEmitter.new(pool.rsc_bundle_hash, output)
    initial_data = build_initial_incremental_request(js_code, emitter)

    # 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)

    response
  end
end

.reset_connectionObject



15
16
17
18
19
20
21
22
# File 'lib/react_on_rails_pro/request.rb', line 15

def reset_connection
  CONNECTION_MUTEX.synchronize do
    new_conn = create_connection
    old_conn = @connection
    @connection = new_conn
    old_conn&.close
  end
end

.upload_assetsObject



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/react_on_rails_pro/request.rb', line 118

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

  form = form_with_assets_and_bundle
  # 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: form)
end