react_on_rails_pro 17.0.0.rc.4 → 17.0.0.rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.controlplane/postgres.yml +7 -2
- data/.controlplane/rails.yml +7 -3
- data/.controlplane/redis.yml +6 -0
- data/Gemfile.lock +3 -3
- data/lib/react_on_rails_pro/async_props_emitter.rb +124 -7
- data/lib/react_on_rails_pro/request.rb +24 -9
- data/lib/react_on_rails_pro/server_rendering_pool/node_rendering_pool.rb +7 -1
- data/lib/react_on_rails_pro/stream_request.rb +73 -18
- data/lib/react_on_rails_pro/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa0bf219fe1d63080c7f653bd3496ea95f15a8e3a1a79a91dd6140de2f8415d9
|
|
4
|
+
data.tar.gz: 8d8ea4b6fd46b87c5411abbd9c01bd6e49ab9fa0bf026dfcf59cfa270a2e00c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e22fcd250f90618ccac39fabb7cbfecb60d502302c3cd1abd0d6bc3f9249c6552b0a25a9dcc235ef4d8c95c11223edc84eb9246bf67058f4ca70074756e9a8b
|
|
7
|
+
data.tar.gz: a6891194c45e31b9ce9a39a9dc31b5cb6807fa5a76bf64844aceaff5bbaacad0eefc9668ceeccdd5d7215c30739a37792918f8355a1094b0cd3339bda8921acd
|
data/.controlplane/postgres.yml
CHANGED
|
@@ -21,13 +21,18 @@ spec:
|
|
|
21
21
|
- path: /var/lib/postgresql/data
|
|
22
22
|
recoveryPolicy: retain
|
|
23
23
|
uri: 'scratch://postgres-vol'
|
|
24
|
-
#
|
|
24
|
+
# Keep Postgres on one manual replica because disk storage is local to one server.
|
|
25
|
+
# Capacity AI may restart the workload when adjusting resources; for Postgres
|
|
26
|
+
# that can disrupt the database despite the retained local volume.
|
|
27
|
+
# Keep the single-replica manual posture explicit instead of relying on platform defaults.
|
|
25
28
|
defaultOptions:
|
|
26
29
|
autoscaling:
|
|
30
|
+
metric: disabled
|
|
31
|
+
minScale: 1
|
|
27
32
|
maxScale: 1
|
|
28
33
|
capacityAI: false
|
|
29
34
|
# This firewall configuration corresponds to using a simple, hard-coded password for postgres
|
|
30
35
|
# in the gvc.yml template.
|
|
31
36
|
firewallConfig:
|
|
32
37
|
internal:
|
|
33
|
-
inboundAllowType: same-gvc
|
|
38
|
+
inboundAllowType: same-gvc
|
data/.controlplane/rails.yml
CHANGED
|
@@ -47,11 +47,15 @@ spec:
|
|
|
47
47
|
- number: 3800
|
|
48
48
|
protocol: http
|
|
49
49
|
defaultOptions:
|
|
50
|
-
#
|
|
50
|
+
# Public/staging test apps keep one warm replica while Capacity AI right-sizes both workload
|
|
51
|
+
# containers (rails and node-renderer).
|
|
52
|
+
# Capacity AI may restart that replica when applying resource changes.
|
|
53
|
+
# For production or high-availability staging, increase maxScale and review Capacity AI suitability.
|
|
51
54
|
autoscaling:
|
|
52
|
-
|
|
55
|
+
metric: disabled
|
|
56
|
+
minScale: 1
|
|
53
57
|
maxScale: 1
|
|
54
|
-
capacityAI:
|
|
58
|
+
capacityAI: true
|
|
55
59
|
firewallConfig:
|
|
56
60
|
external:
|
|
57
61
|
# Default to allow public access to Rails server
|
data/.controlplane/redis.yml
CHANGED
|
@@ -22,7 +22,13 @@ spec:
|
|
|
22
22
|
- number: 6379
|
|
23
23
|
protocol: tcp
|
|
24
24
|
defaultOptions:
|
|
25
|
+
# Capacity AI may restart the workload when adjusting resources; for Redis
|
|
26
|
+
# that can disrupt cache state and connections.
|
|
27
|
+
# Review cache restart tolerance before copying the Rails Capacity AI posture here.
|
|
28
|
+
# Keep the single-replica manual posture explicit instead of relying on platform defaults.
|
|
25
29
|
autoscaling:
|
|
30
|
+
metric: disabled
|
|
31
|
+
minScale: 1
|
|
26
32
|
maxScale: 1
|
|
27
33
|
capacityAI: false
|
|
28
34
|
# This firewall configuration corresponds to using no password for Redis in the gvc.yml template.
|
data/Gemfile.lock
CHANGED
|
@@ -9,7 +9,7 @@ GIT
|
|
|
9
9
|
PATH
|
|
10
10
|
remote: ..
|
|
11
11
|
specs:
|
|
12
|
-
react_on_rails (17.0.0.rc.
|
|
12
|
+
react_on_rails (17.0.0.rc.6)
|
|
13
13
|
addressable
|
|
14
14
|
connection_pool
|
|
15
15
|
execjs (~> 2.5)
|
|
@@ -20,7 +20,7 @@ PATH
|
|
|
20
20
|
PATH
|
|
21
21
|
remote: .
|
|
22
22
|
specs:
|
|
23
|
-
react_on_rails_pro (17.0.0.rc.
|
|
23
|
+
react_on_rails_pro (17.0.0.rc.6)
|
|
24
24
|
addressable
|
|
25
25
|
async (>= 2.29)
|
|
26
26
|
async-http (~> 0.95)
|
|
@@ -28,7 +28,7 @@ PATH
|
|
|
28
28
|
io-endpoint (~> 0.17.0)
|
|
29
29
|
jwt (>= 2.5, < 4)
|
|
30
30
|
rainbow
|
|
31
|
-
react_on_rails (= 17.0.0.rc.
|
|
31
|
+
react_on_rails (= 17.0.0.rc.6)
|
|
32
32
|
|
|
33
33
|
GEM
|
|
34
34
|
remote: https://rubygems.org/
|
|
@@ -28,15 +28,40 @@ module ReactOnRailsPro
|
|
|
28
28
|
# Global variables in Node.js VM persist across requests, causing data leakage.
|
|
29
29
|
# sharedExecutionContext is scoped to a single HTTP request (ExecutionContext).
|
|
30
30
|
#
|
|
31
|
-
#
|
|
31
|
+
# PULL MODE:
|
|
32
|
+
# When pull_enabled is true, React components can request props lazily via
|
|
33
|
+
# getProp(). Those requests arrive as propRequest chunks on the response stream.
|
|
34
|
+
# `pull_requests` exposes an Async::Queue that yields prop names as they arrive.
|
|
35
|
+
# The user's block can dequeue and resolve them dynamically.
|
|
36
|
+
#
|
|
37
|
+
# @example Push-only usage (existing)
|
|
32
38
|
# stream_react_component_with_async_props("Dashboard") do |emit|
|
|
33
|
-
# emit.call("users", User.all.to_a)
|
|
34
|
-
# emit.call("posts", Post.recent.to_a)
|
|
39
|
+
# emit.call("users", User.all.to_a)
|
|
40
|
+
# emit.call("posts", Post.recent.to_a)
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# @example Pull mode usage
|
|
44
|
+
# stream_react_component_with_async_props("Dashboard", push_props: %w[stats]) do |emit|
|
|
45
|
+
# emit.call("stats", compute_stats)
|
|
46
|
+
# while (prop_name = emit.pull_requests.dequeue)
|
|
47
|
+
# emit.call(prop_name, fetch_prop(prop_name))
|
|
48
|
+
# end
|
|
35
49
|
# end
|
|
36
50
|
class AsyncPropsEmitter
|
|
37
|
-
|
|
51
|
+
SANITIZED_REJECTION_REASON = "Async prop rejected by server"
|
|
52
|
+
|
|
53
|
+
attr_reader :pull_requests
|
|
54
|
+
|
|
55
|
+
def initialize(bundle_timestamp, request_stream, pull_enabled: false)
|
|
38
56
|
@bundle_timestamp = bundle_timestamp
|
|
39
57
|
@request_stream = request_stream
|
|
58
|
+
@pushed_props = Set.new
|
|
59
|
+
@pull_enabled = pull_enabled
|
|
60
|
+
@pull_requests = PullRequestQueue.new(@pushed_props) if pull_enabled
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def pull_enabled?
|
|
64
|
+
@pull_enabled
|
|
40
65
|
end
|
|
41
66
|
|
|
42
67
|
# Sends an async prop to the Node renderer.
|
|
@@ -45,17 +70,35 @@ module ReactOnRailsPro
|
|
|
45
70
|
def call(prop_name, prop_value)
|
|
46
71
|
update_chunk = generate_update_chunk(prop_name, prop_value)
|
|
47
72
|
@request_stream << "#{update_chunk.to_json}\n"
|
|
73
|
+
@pushed_props.add(prop_name)
|
|
48
74
|
rescue StandardError => e
|
|
75
|
+
# Continue streaming: one failed async prop write should not abort the
|
|
76
|
+
# entire render. The prop is not marked as pushed unless the write
|
|
77
|
+
# succeeds, so pull mode can request it again instead of silently hanging.
|
|
49
78
|
Rails.logger.error do
|
|
50
79
|
backtrace = e.backtrace&.first(5)&.join("\n")
|
|
51
80
|
"[ReactOnRailsPro::AsyncProps] Failed to send async prop '#{prop_name}': " \
|
|
52
81
|
"#{e.class} - #{e.message}\n#{backtrace}"
|
|
53
82
|
end
|
|
54
|
-
# Continue - don't abort entire render because one prop failed
|
|
55
83
|
end
|
|
56
84
|
|
|
57
|
-
#
|
|
58
|
-
|
|
85
|
+
# Rejects an async prop on the Node side so React can show an error boundary.
|
|
86
|
+
def reject(prop_name, reason)
|
|
87
|
+
update_chunk = generate_reject_chunk(prop_name, reason)
|
|
88
|
+
@request_stream << "#{update_chunk.to_json}\n"
|
|
89
|
+
# Once the reject chunk is written, Ruby treats the prop as settled too.
|
|
90
|
+
# That keeps duplicate pull requests filtered even if the JS manager is recreated.
|
|
91
|
+
@pushed_props.add(prop_name)
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
Rails.logger.error do
|
|
94
|
+
backtrace = e.backtrace&.first(5)&.join("\n")
|
|
95
|
+
"[ReactOnRailsPro::AsyncProps] Failed to reject async prop '#{prop_name}': " \
|
|
96
|
+
"#{e.class} - #{e.message}\n#{backtrace}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Generates the chunk that should be executed when the request stream closes.
|
|
101
|
+
# This tells the asyncPropsManager to end the stream.
|
|
59
102
|
def end_stream_chunk
|
|
60
103
|
{
|
|
61
104
|
bundleTimestamp: @bundle_timestamp,
|
|
@@ -63,6 +106,12 @@ module ReactOnRailsPro
|
|
|
63
106
|
}
|
|
64
107
|
end
|
|
65
108
|
|
|
109
|
+
# Called by stream_request when the response stream signals render complete.
|
|
110
|
+
# Closes the pull_requests queue so dequeue returns nil.
|
|
111
|
+
def render_complete!
|
|
112
|
+
@pull_requests&.close
|
|
113
|
+
end
|
|
114
|
+
|
|
66
115
|
private
|
|
67
116
|
|
|
68
117
|
def generate_update_chunk(prop_name, value)
|
|
@@ -72,6 +121,13 @@ module ReactOnRailsPro
|
|
|
72
121
|
}
|
|
73
122
|
end
|
|
74
123
|
|
|
124
|
+
def generate_reject_chunk(prop_name, reason)
|
|
125
|
+
{
|
|
126
|
+
bundleTimestamp: @bundle_timestamp,
|
|
127
|
+
updateChunk: generate_reject_prop_js(prop_name, reason)
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
75
131
|
def generate_set_prop_js(prop_name, value)
|
|
76
132
|
<<~JS.strip
|
|
77
133
|
(function(){
|
|
@@ -81,6 +137,25 @@ module ReactOnRailsPro
|
|
|
81
137
|
JS
|
|
82
138
|
end
|
|
83
139
|
|
|
140
|
+
def generate_reject_prop_js(prop_name, reason)
|
|
141
|
+
<<~JS.strip
|
|
142
|
+
(function(){
|
|
143
|
+
var asyncPropsManager = ReactOnRails.getOrCreateAsyncPropsManager(sharedExecutionContext);
|
|
144
|
+
asyncPropsManager.rejectProp(#{prop_name.to_json}, #{sanitized_rejection_reason(reason).to_json});
|
|
145
|
+
})()
|
|
146
|
+
JS
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Always return the generic message regardless of the internal reason. Raw
|
|
150
|
+
# Rails-side details such as SQL errors, file paths, or credentials must not
|
|
151
|
+
# reach the browser. The raw reason is still emitted to debug logs for
|
|
152
|
+
# operators; keep it below info level because staging log aggregators may
|
|
153
|
+
# persist those details.
|
|
154
|
+
def sanitized_rejection_reason(reason)
|
|
155
|
+
Rails.logger.debug { "[ReactOnRailsPro::AsyncProps] Prop rejected (internal reason): #{reason}" }
|
|
156
|
+
SANITIZED_REJECTION_REASON
|
|
157
|
+
end
|
|
158
|
+
|
|
84
159
|
def generate_end_stream_js
|
|
85
160
|
<<~JS.strip
|
|
86
161
|
(function(){
|
|
@@ -90,4 +165,46 @@ module ReactOnRailsPro
|
|
|
90
165
|
JS
|
|
91
166
|
end
|
|
92
167
|
end
|
|
168
|
+
|
|
169
|
+
# Queue of prop names requested by React (pull mode).
|
|
170
|
+
# Wraps Async::Queue with automatic filtering of already-pushed props.
|
|
171
|
+
# dequeue returns nil after the queue is closed (render complete).
|
|
172
|
+
class PullRequestQueue
|
|
173
|
+
def initialize(pushed_props)
|
|
174
|
+
@queue = Async::Queue.new
|
|
175
|
+
@pushed_props = pushed_props
|
|
176
|
+
@closed = false
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Enqueue a propRequest from the Node renderer.
|
|
180
|
+
# Silently drops requests for props that have already been pushed.
|
|
181
|
+
def enqueue(prop_name)
|
|
182
|
+
# @pushed_props is mutated by AsyncPropsEmitter#call. In fiber-concurrent
|
|
183
|
+
# code this has a narrow TOCTOU window; duplicate requests are filtered on
|
|
184
|
+
# the TypeScript side via AsyncPropsManager's pullRequested flag.
|
|
185
|
+
return if @closed || @pushed_props.include?(prop_name)
|
|
186
|
+
|
|
187
|
+
@queue.enqueue(prop_name)
|
|
188
|
+
rescue Async::Queue::ClosedError
|
|
189
|
+
# Queue closed between the @closed guard and enqueue; safe to ignore.
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Blocks until a prop name is available, or returns nil if closed.
|
|
193
|
+
def dequeue
|
|
194
|
+
@queue.dequeue
|
|
195
|
+
rescue Async::Queue::ClosedError
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def close
|
|
200
|
+
return if @closed
|
|
201
|
+
|
|
202
|
+
@queue.close
|
|
203
|
+
@closed = true
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def closed?
|
|
207
|
+
@closed
|
|
208
|
+
end
|
|
209
|
+
end
|
|
93
210
|
end
|
|
@@ -87,13 +87,19 @@ module ReactOnRailsPro
|
|
|
87
87
|
# - When the block finishes, we close the output (END_STREAM flag)
|
|
88
88
|
# - Node's handleRequestClosed then calls asyncPropsManager.endStream()
|
|
89
89
|
#
|
|
90
|
-
def render_code_with_incremental_updates(path, js_code, async_props_block:)
|
|
90
|
+
def render_code_with_incremental_updates(path, js_code, async_props_block:, pull_enabled: false, push_props: nil)
|
|
91
91
|
Rails.logger.info { "[ReactOnRailsPro] Perform incremental rendering request #{path}" }
|
|
92
92
|
|
|
93
93
|
pool = ReactOnRailsPro::ServerRenderingPool::NodeRenderingPool
|
|
94
|
+
if !push_props.nil? && !pull_enabled
|
|
95
|
+
raise ArgumentError, "push_props can only be provided when pull_enabled is true"
|
|
96
|
+
end
|
|
94
97
|
|
|
95
98
|
warn_cb = ->(request_time) { warn_if_slow_streaming_first_chunk(path, request_time) }
|
|
96
|
-
ReactOnRailsPro::StreamRequest.create(
|
|
99
|
+
ReactOnRailsPro::StreamRequest.create(
|
|
100
|
+
first_chunk_warn_callback: warn_cb,
|
|
101
|
+
pull_enabled:
|
|
102
|
+
) do |send_bundle, tasks|
|
|
97
103
|
if send_bundle
|
|
98
104
|
Rails.logger.info { "[ReactOnRailsPro] Sending bundle to the node renderer" }
|
|
99
105
|
upload_assets
|
|
@@ -106,10 +112,14 @@ module ReactOnRailsPro
|
|
|
106
112
|
headers: [["content-type", "application/x-ndjson"]]
|
|
107
113
|
)
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
emitter = ReactOnRailsPro::AsyncPropsEmitter.new(
|
|
116
|
+
pool.rsc_bundle_hash,
|
|
117
|
+
output,
|
|
118
|
+
pull_enabled:
|
|
119
|
+
)
|
|
120
|
+
initial_data = build_initial_incremental_request(
|
|
121
|
+
js_code, emitter, pull_enabled:, push_props:
|
|
122
|
+
)
|
|
113
123
|
|
|
114
124
|
# Send the initial render request as first NDJSON line
|
|
115
125
|
output << "#{initial_data.to_json}\n"
|
|
@@ -124,7 +134,7 @@ module ReactOnRailsPro
|
|
|
124
134
|
output.close
|
|
125
135
|
end)
|
|
126
136
|
|
|
127
|
-
response
|
|
137
|
+
{ pull_result: true, response:, emitter: pull_enabled ? emitter : nil }
|
|
128
138
|
end
|
|
129
139
|
end
|
|
130
140
|
|
|
@@ -342,11 +352,16 @@ module ReactOnRailsPro
|
|
|
342
352
|
ReactOnRailsPro::Utils.common_form_data
|
|
343
353
|
end
|
|
344
354
|
|
|
345
|
-
def build_initial_incremental_request(js_code, emitter)
|
|
346
|
-
common_form_data.merge(
|
|
355
|
+
def build_initial_incremental_request(js_code, emitter, pull_enabled: false, push_props: nil)
|
|
356
|
+
data = common_form_data.merge(
|
|
347
357
|
renderingRequest: js_code,
|
|
348
358
|
onRequestClosedUpdateChunk: emitter.end_stream_chunk
|
|
349
359
|
)
|
|
360
|
+
if pull_enabled
|
|
361
|
+
data[:pullEnabled] = true
|
|
362
|
+
data[:pushProps] = Array(push_props)
|
|
363
|
+
end
|
|
364
|
+
data
|
|
350
365
|
end
|
|
351
366
|
|
|
352
367
|
def create_connection
|
|
@@ -72,10 +72,16 @@ module ReactOnRailsPro
|
|
|
72
72
|
if async_props_block
|
|
73
73
|
# Use incremental rendering when async props block is provided
|
|
74
74
|
path = prepare_incremental_render_path(js_code, render_options)
|
|
75
|
+
push_props = render_options.internal_option(:push_props)
|
|
76
|
+
# Pull mode is enabled whenever push_props is set, including [] for pure pull.
|
|
77
|
+
# nil means push-only mode with no bidirectional prop-request channel.
|
|
78
|
+
pull_enabled = !push_props.nil?
|
|
75
79
|
ReactOnRailsPro::Request.render_code_with_incremental_updates(
|
|
76
80
|
path,
|
|
77
81
|
js_code,
|
|
78
|
-
async_props_block
|
|
82
|
+
async_props_block:,
|
|
83
|
+
pull_enabled:,
|
|
84
|
+
push_props:
|
|
79
85
|
)
|
|
80
86
|
else
|
|
81
87
|
# Use standard streaming when no async props block
|
|
@@ -110,17 +110,21 @@ module ReactOnRailsPro
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
class StreamRequest
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
MAX_PULL_PROP_NAME_LENGTH = 256
|
|
114
|
+
# Keep aligned with ReactOnRails::LengthPrefixedParser::CONTROL_MESSAGE_TYPES,
|
|
115
|
+
# which parses these same control frames from the shared wire format.
|
|
116
|
+
CONTROL_MESSAGE_TYPES = %w[propRequest renderComplete].freeze
|
|
117
|
+
private_constant :CONTROL_MESSAGE_TYPES
|
|
116
118
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
def http_status = @status
|
|
120
|
+
|
|
121
|
+
def http_status_recorded? = @status_recorded
|
|
120
122
|
|
|
121
|
-
def initialize(first_chunk_warn_callback: nil, &request_block)
|
|
123
|
+
def initialize(first_chunk_warn_callback: nil, pull_enabled: false, &request_block)
|
|
122
124
|
@request_executor = request_block
|
|
123
125
|
@first_chunk_warn_callback = first_chunk_warn_callback
|
|
126
|
+
@pull_enabled = pull_enabled
|
|
127
|
+
@emitter = nil
|
|
124
128
|
@status = nil
|
|
125
129
|
@status_recorded = false
|
|
126
130
|
end
|
|
@@ -133,8 +137,8 @@ module ReactOnRailsPro
|
|
|
133
137
|
Sync { consume_with_bundle_reupload(&block) }
|
|
134
138
|
end
|
|
135
139
|
|
|
136
|
-
def self.create(first_chunk_warn_callback: nil, &request_block)
|
|
137
|
-
StreamDecorator.new(new(first_chunk_warn_callback:, &request_block))
|
|
140
|
+
def self.create(first_chunk_warn_callback: nil, pull_enabled: false, &request_block)
|
|
141
|
+
StreamDecorator.new(new(first_chunk_warn_callback:, pull_enabled:, &request_block))
|
|
138
142
|
end
|
|
139
143
|
|
|
140
144
|
private
|
|
@@ -151,9 +155,19 @@ module ReactOnRailsPro
|
|
|
151
155
|
# response attempt.
|
|
152
156
|
reset_response_status
|
|
153
157
|
@received_first_chunk = false
|
|
154
|
-
stream_response = @request_executor.call(send_bundle, tasks)
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
stream_response, @emitter = normalize_executor_result(@request_executor.call(send_bundle, tasks))
|
|
159
|
+
|
|
160
|
+
begin
|
|
161
|
+
process_response_chunks(stream_response, &block)
|
|
162
|
+
ensure
|
|
163
|
+
# renderComplete control messages normally close this queue earlier.
|
|
164
|
+
# This safety net covers parser errors, stream aborts, and timeouts
|
|
165
|
+
# before renderComplete arrives. The user's pull_requests.dequeue loop
|
|
166
|
+
# then exits with nil; any in-flight emit.call is protected by
|
|
167
|
+
# AsyncPropsEmitter#call's rescue.
|
|
168
|
+
@emitter&.render_complete!
|
|
169
|
+
@emitter = nil
|
|
170
|
+
end
|
|
157
171
|
break
|
|
158
172
|
rescue ReactOnRailsPro::RendererHttpClient::HTTPError => e
|
|
159
173
|
stop_tasks(tasks) if retrying_with_bundle_upload?(e, send_bundle)
|
|
@@ -182,12 +196,9 @@ module ReactOnRailsPro
|
|
|
182
196
|
record_status(stream_response) unless @status_recorded
|
|
183
197
|
next if stream_response.error?
|
|
184
198
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
parser.feed(chunk, &block)
|
|
199
|
+
yielded_content = parse_and_route_chunk(parser, chunk, &block)
|
|
200
|
+
# Control messages are protocol bookkeeping, so they do not satisfy the slow-first-chunk marker.
|
|
201
|
+
record_first_chunk(request_start_time) if yielded_content
|
|
191
202
|
end
|
|
192
203
|
record_status(stream_response) unless @status_recorded
|
|
193
204
|
parser.flush
|
|
@@ -199,6 +210,50 @@ module ReactOnRailsPro
|
|
|
199
210
|
raise
|
|
200
211
|
end
|
|
201
212
|
|
|
213
|
+
# Expected shapes from callers:
|
|
214
|
+
# render_code_with_incremental_updates => { pull_result: true, response:, emitter: }
|
|
215
|
+
# render_code_as_stream => bare response object (pre-pull mode)
|
|
216
|
+
def normalize_executor_result(result)
|
|
217
|
+
# Incremental rendering returns a named result so unrelated Array-like responses are never
|
|
218
|
+
# mistaken for [response, emitter]. Older renderers still return only response. The
|
|
219
|
+
# :pull_result key is the protocol discriminator, so future response-shaped hashes are safe.
|
|
220
|
+
return result.values_at(:response, :emitter) if result.is_a?(Hash) && result.key?(:pull_result)
|
|
221
|
+
|
|
222
|
+
[result, nil]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def record_first_chunk(request_start_time)
|
|
226
|
+
return if @received_first_chunk
|
|
227
|
+
|
|
228
|
+
@received_first_chunk = true
|
|
229
|
+
@first_chunk_warn_callback&.call(Time.now - request_start_time)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def parse_and_route_chunk(parser, chunk, &)
|
|
233
|
+
yielded_content = false
|
|
234
|
+
parser.feed(chunk) do |parsed|
|
|
235
|
+
next route_control_message(parsed) if CONTROL_MESSAGE_TYPES.include?(parsed["messageType"])
|
|
236
|
+
|
|
237
|
+
yield parsed
|
|
238
|
+
yielded_content = true
|
|
239
|
+
end
|
|
240
|
+
yielded_content
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def route_control_message(parsed)
|
|
244
|
+
return unless @emitter
|
|
245
|
+
|
|
246
|
+
case parsed["messageType"]
|
|
247
|
+
when "propRequest"
|
|
248
|
+
prop_name = parsed["propName"]
|
|
249
|
+
@emitter.pull_requests&.enqueue(prop_name) if prop_name.is_a?(String) &&
|
|
250
|
+
!prop_name.empty? &&
|
|
251
|
+
prop_name.length <= MAX_PULL_PROP_NAME_LENGTH
|
|
252
|
+
when "renderComplete"
|
|
253
|
+
@emitter.render_complete!
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
202
257
|
# Retrying after first chunk would duplicate content in the page.
|
|
203
258
|
def raise_or_retry_streaming_transport_error(error, available_retries)
|
|
204
259
|
error_type = error.is_a?(ReactOnRailsPro::RendererHttpClient::TimeoutError) ? "Time out" : "Connection"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: react_on_rails_pro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 17.0.0.rc.
|
|
4
|
+
version: 17.0.0.rc.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Gordon
|
|
@@ -119,14 +119,14 @@ dependencies:
|
|
|
119
119
|
requirements:
|
|
120
120
|
- - '='
|
|
121
121
|
- !ruby/object:Gem::Version
|
|
122
|
-
version: 17.0.0.rc.
|
|
122
|
+
version: 17.0.0.rc.6
|
|
123
123
|
type: :runtime
|
|
124
124
|
prerelease: false
|
|
125
125
|
version_requirements: !ruby/object:Gem::Requirement
|
|
126
126
|
requirements:
|
|
127
127
|
- - '='
|
|
128
128
|
- !ruby/object:Gem::Version
|
|
129
|
-
version: 17.0.0.rc.
|
|
129
|
+
version: 17.0.0.rc.6
|
|
130
130
|
- !ruby/object:Gem::Dependency
|
|
131
131
|
name: bundler
|
|
132
132
|
requirement: !ruby/object:Gem::Requirement
|