react_on_rails_pro 16.4.0.rc.6 → 16.4.0.rc.8
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/CONTRIBUTING.md +4 -4
- data/Gemfile.development_dependencies +1 -1
- data/Gemfile.lock +20 -18
- data/README.md +16 -16
- data/app/helpers/react_on_rails_pro_helper.rb +20 -8
- data/lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb +16 -1
- data/lib/react_on_rails_pro/concerns/stream.rb +49 -12
- data/lib/react_on_rails_pro/engine.rb +58 -0
- data/lib/react_on_rails_pro/request.rb +10 -0
- data/lib/react_on_rails_pro/stream_request.rb +10 -1
- data/lib/react_on_rails_pro/version.rb +1 -1
- metadata +5 -39
- data/docs/bundle-caching.md +0 -219
- data/docs/caching.md +0 -246
- data/docs/code-splitting-loadable-components.md +0 -326
- data/docs/configuration.md +0 -165
- data/docs/contributors-info/onboarding-customers.md +0 -7
- data/docs/contributors-info/releasing.md +0 -41
- data/docs/contributors-info/style.md +0 -41
- data/docs/home-pro.md +0 -164
- data/docs/installation.md +0 -309
- data/docs/js-memory-leaks.md +0 -21
- data/docs/node-renderer/basics.md +0 -94
- data/docs/node-renderer/debugging.md +0 -42
- data/docs/node-renderer/error-reporting-and-tracing.md +0 -172
- data/docs/node-renderer/heroku.md +0 -101
- data/docs/node-renderer/js-configuration.md +0 -163
- data/docs/node-renderer/troubleshooting.md +0 -5
- data/docs/profiling-server-side-rendering-code.md +0 -180
- data/docs/react-server-components/add-streaming-and-interactivity.md +0 -190
- data/docs/react-server-components/create-without-ssr.md +0 -448
- data/docs/react-server-components/flight-protocol-syntax.md +0 -294
- data/docs/react-server-components/glossary.md +0 -121
- data/docs/react-server-components/how-react-server-components-work.md +0 -250
- data/docs/react-server-components/inside-client-components.md +0 -333
- data/docs/react-server-components/purpose-and-benefits.md +0 -253
- data/docs/react-server-components/rendering-flow.md +0 -90
- data/docs/react-server-components/selective-hydration-in-streamed-components.md +0 -75
- data/docs/react-server-components/server-side-rendering.md +0 -73
- data/docs/react-server-components/tutorial.md +0 -19
- data/docs/release-notes/4.0.md +0 -103
- data/docs/release-notes/v4-react-server-components.md +0 -66
- data/docs/ruby-api.md +0 -9
- data/docs/streaming-server-rendering.md +0 -208
- data/docs/troubleshooting.md +0 -3
- data/docs/updating.md +0 -273
- /data/app/views/react_on_rails_pro/{rsc_payload.html.erb → rsc_payload.text.erb} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 717ead6ca9b9c1f7354820f063551a8403cabb9fafeb095bd46f35e3d368a483
|
|
4
|
+
data.tar.gz: 5062b202868b9106090becb6dd5f040761a2dd37ec2a1323053e43df111044ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2fc8c9ad613776be17f834f8a7c2d7ff07b123e6e703b8dd21ffdc4687ee51d0037b49c813c7f7b9596266bfa15885336b1f6ce9df6530e995c68d58557d745
|
|
7
|
+
data.tar.gz: 025cf054e18a200c61ad22199617ce0374889b1f825290ee13952f02bb993532cf8b1738c0df4a7295c4b9bf576782bf42abe04395f9071dd46b0686697c8c5b
|
data/CONTRIBUTING.md
CHANGED
|
@@ -47,12 +47,12 @@ From [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/
|
|
|
47
47
|
## Doc Changes
|
|
48
48
|
|
|
49
49
|
When making doc changes, we want the change to work on both [the ShakaCode docs site](https://www.shakacode.com/react-on-rails-pro/docs/) and when browsing the GitHub repo.
|
|
50
|
-
The issue is that the
|
|
50
|
+
The issue is that the ShakaCode site is generated only from files in [`../docs/pro`](../docs/pro), so any references from them to non-doc files must use the full GitHub URL.
|
|
51
51
|
|
|
52
52
|
### Links to other docs:
|
|
53
53
|
|
|
54
54
|
- When making references to doc files, use a relative URL path like:
|
|
55
|
-
`[Installation
|
|
55
|
+
`[Installation Guide](../docs/pro/installation.md)`
|
|
56
56
|
|
|
57
57
|
- When making references to source code files, use a full url path like:
|
|
58
58
|
`[spec/dummy/config/initializers/react_on_rails.rb](https://github.com/shakacode/react_on_rails/tree/master/react_on_rails_pro/spec/dummy/config/initializers/react_on_rails.rb)`
|
|
@@ -119,7 +119,7 @@ script/ci-changes-detector origin/master
|
|
|
119
119
|
- Push Pro changes without testing locally first
|
|
120
120
|
- Modify both Pro and main gem without running full tests
|
|
121
121
|
|
|
122
|
-
For comprehensive CI documentation, see [`../
|
|
122
|
+
For comprehensive CI documentation, see [`../internal/contributor-info/ci-optimization.md`](../internal/contributor-info/ci-optimization.md) in the repository root.
|
|
123
123
|
|
|
124
124
|
# IDE/Editor Setup
|
|
125
125
|
|
|
@@ -395,5 +395,5 @@ rake release[17.0.0,false,verdaccio]
|
|
|
395
395
|
|
|
396
396
|
For complete documentation, see:
|
|
397
397
|
|
|
398
|
-
- [Root Release Documentation](../
|
|
398
|
+
- [Root Release Documentation](../internal/contributor-info/releasing.md)
|
|
399
399
|
- Run `rake -D release` for inline help
|
data/Gemfile.lock
CHANGED
|
@@ -9,7 +9,7 @@ GIT
|
|
|
9
9
|
PATH
|
|
10
10
|
remote: ..
|
|
11
11
|
specs:
|
|
12
|
-
react_on_rails (16.4.0.rc.
|
|
12
|
+
react_on_rails (16.4.0.rc.8)
|
|
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 (16.4.0.rc.
|
|
23
|
+
react_on_rails_pro (16.4.0.rc.8)
|
|
24
24
|
addressable
|
|
25
25
|
async (>= 2.6)
|
|
26
26
|
connection_pool
|
|
@@ -29,7 +29,7 @@ PATH
|
|
|
29
29
|
httpx (~> 1.5)
|
|
30
30
|
jwt (~> 2.7)
|
|
31
31
|
rainbow
|
|
32
|
-
react_on_rails (= 16.4.0.rc.
|
|
32
|
+
react_on_rails (= 16.4.0.rc.8)
|
|
33
33
|
|
|
34
34
|
GEM
|
|
35
35
|
remote: https://rubygems.org/
|
|
@@ -165,7 +165,7 @@ GEM
|
|
|
165
165
|
drb (2.2.3)
|
|
166
166
|
equivalent-xml (0.6.0)
|
|
167
167
|
nokogiri (>= 1.4.3)
|
|
168
|
-
erb (6.0.
|
|
168
|
+
erb (6.0.2)
|
|
169
169
|
erubi (1.13.1)
|
|
170
170
|
execjs (2.10.0)
|
|
171
171
|
fakefs (2.8.0)
|
|
@@ -194,8 +194,9 @@ GEM
|
|
|
194
194
|
concurrent-ruby (~> 1.0)
|
|
195
195
|
io-console (0.8.2)
|
|
196
196
|
io-event (1.14.2)
|
|
197
|
-
irb (1.
|
|
197
|
+
irb (1.17.0)
|
|
198
198
|
pp (>= 0.6.0)
|
|
199
|
+
prism (>= 1.3.0)
|
|
199
200
|
rdoc (>= 4.0.0)
|
|
200
201
|
reline (>= 0.4.2)
|
|
201
202
|
jbuilder (2.12.0)
|
|
@@ -230,7 +231,8 @@ GEM
|
|
|
230
231
|
method_source (1.1.0)
|
|
231
232
|
metrics (0.15.0)
|
|
232
233
|
mini_mime (1.1.5)
|
|
233
|
-
minitest (6.0.
|
|
234
|
+
minitest (6.0.2)
|
|
235
|
+
drb (~> 2.0)
|
|
234
236
|
prism (~> 1.5)
|
|
235
237
|
mize (0.4.1)
|
|
236
238
|
protocol (~> 2.0)
|
|
@@ -247,11 +249,11 @@ GEM
|
|
|
247
249
|
net-smtp (0.5.1)
|
|
248
250
|
net-protocol
|
|
249
251
|
nio4r (2.7.5)
|
|
250
|
-
nokogiri (1.19.
|
|
252
|
+
nokogiri (1.19.1-arm64-darwin)
|
|
251
253
|
racc (~> 1.4)
|
|
252
|
-
nokogiri (1.19.
|
|
254
|
+
nokogiri (1.19.1-x86_64-darwin)
|
|
253
255
|
racc (~> 1.4)
|
|
254
|
-
nokogiri (1.19.
|
|
256
|
+
nokogiri (1.19.1-x86_64-linux-gnu)
|
|
255
257
|
racc (~> 1.4)
|
|
256
258
|
package_json (0.2.0)
|
|
257
259
|
parallel (1.27.0)
|
|
@@ -262,7 +264,7 @@ GEM
|
|
|
262
264
|
pp (0.6.3)
|
|
263
265
|
prettyprint
|
|
264
266
|
prettyprint (0.2.0)
|
|
265
|
-
prism (1.
|
|
267
|
+
prism (1.9.0)
|
|
266
268
|
protocol (2.0.0)
|
|
267
269
|
ruby_parser (~> 3.0)
|
|
268
270
|
pry (0.14.2)
|
|
@@ -282,7 +284,7 @@ GEM
|
|
|
282
284
|
puma (6.5.0)
|
|
283
285
|
nio4r (~> 2.0)
|
|
284
286
|
racc (1.8.1)
|
|
285
|
-
rack (3.2.
|
|
287
|
+
rack (3.2.5)
|
|
286
288
|
rack-proxy (0.7.7)
|
|
287
289
|
rack
|
|
288
290
|
rack-session (2.1.1)
|
|
@@ -310,8 +312,8 @@ GEM
|
|
|
310
312
|
activesupport (>= 5.0.0)
|
|
311
313
|
minitest
|
|
312
314
|
nokogiri (>= 1.6)
|
|
313
|
-
rails-html-sanitizer (1.
|
|
314
|
-
loofah (~> 2.
|
|
315
|
+
rails-html-sanitizer (1.7.0)
|
|
316
|
+
loofah (~> 2.25)
|
|
315
317
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
|
316
318
|
railties (7.2.3)
|
|
317
319
|
actionpack (= 7.2.3)
|
|
@@ -330,7 +332,7 @@ GEM
|
|
|
330
332
|
ffi (~> 1.0)
|
|
331
333
|
rbs (3.9.5)
|
|
332
334
|
logger
|
|
333
|
-
rdoc (7.
|
|
335
|
+
rdoc (7.2.0)
|
|
334
336
|
erb
|
|
335
337
|
psych (>= 4.0.0)
|
|
336
338
|
tsort
|
|
@@ -414,9 +416,9 @@ GEM
|
|
|
414
416
|
rexml (~> 3.2, >= 3.2.5)
|
|
415
417
|
rubyzip (>= 1.2.2, < 3.0)
|
|
416
418
|
websocket (~> 1.0)
|
|
417
|
-
semantic_range (3.1.
|
|
419
|
+
semantic_range (3.1.1)
|
|
418
420
|
sexp_processor (4.17.1)
|
|
419
|
-
shakapacker (9.
|
|
421
|
+
shakapacker (9.6.1)
|
|
420
422
|
activesupport (>= 5.2)
|
|
421
423
|
package_json
|
|
422
424
|
rack-proxy (>= 0.6.1)
|
|
@@ -483,7 +485,7 @@ GEM
|
|
|
483
485
|
xpath (3.2.0)
|
|
484
486
|
nokogiri (~> 1.8)
|
|
485
487
|
yard (0.9.36)
|
|
486
|
-
zeitwerk (2.7.
|
|
488
|
+
zeitwerk (2.7.5)
|
|
487
489
|
|
|
488
490
|
PLATFORMS
|
|
489
491
|
arm64-darwin-24
|
|
@@ -531,7 +533,7 @@ DEPENDENCIES
|
|
|
531
533
|
sass-rails
|
|
532
534
|
scss_lint
|
|
533
535
|
selenium-webdriver (= 4.9.0)
|
|
534
|
-
shakapacker (= 9.
|
|
536
|
+
shakapacker (= 9.6.1)
|
|
535
537
|
spring
|
|
536
538
|
spring-watcher-listen
|
|
537
539
|
sprockets
|
data/README.md
CHANGED
|
@@ -156,7 +156,7 @@ end %>
|
|
|
156
156
|
- Automatic cache invalidation based on props
|
|
157
157
|
- Works with Rails fragment caching infrastructure
|
|
158
158
|
|
|
159
|
-
**📖 Learn more**: [docs/caching.md](
|
|
159
|
+
**📖 Learn more**: [docs/pro/caching.md](../docs/pro/caching.md)
|
|
160
160
|
|
|
161
161
|
### 2. Prerender Caching
|
|
162
162
|
|
|
@@ -175,7 +175,7 @@ end
|
|
|
175
175
|
- Caches across multiple requests
|
|
176
176
|
- Complements fragment caching for maximum performance
|
|
177
177
|
|
|
178
|
-
**📖 Learn more**: [docs/caching.md](
|
|
178
|
+
**📖 Learn more**: [docs/pro/caching.md](../docs/pro/caching.md)
|
|
179
179
|
|
|
180
180
|
### 3. React on Rails Pro Node Renderer
|
|
181
181
|
|
|
@@ -203,7 +203,7 @@ reactOnRailsProNodeRenderer({
|
|
|
203
203
|
});
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
-
**📖 Learn more**: [docs/node-renderer/basics.md](
|
|
206
|
+
**📖 Learn more**: [docs/pro/node-renderer/basics.md](../docs/pro/node-renderer/basics.md)
|
|
207
207
|
|
|
208
208
|
### 4. React Server Components (RSC)
|
|
209
209
|
|
|
@@ -236,7 +236,7 @@ Speed up webpack rebuilds by caching unchanged bundles.
|
|
|
236
236
|
- **Faster development**: Hot reload only what changed
|
|
237
237
|
- **Lower costs**: Reduce build server time
|
|
238
238
|
|
|
239
|
-
**📖 Learn more**: [docs/bundle-caching.md](
|
|
239
|
+
**📖 Learn more**: [docs/pro/bundle-caching.md](../docs/pro/bundle-caching.md)
|
|
240
240
|
|
|
241
241
|
### 6. Global State Management
|
|
242
242
|
|
|
@@ -249,7 +249,7 @@ ReactOnRailsPro.configure do |config|
|
|
|
249
249
|
end
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
-
**📖 Learn more**: [docs/configuration.md](
|
|
252
|
+
**📖 Learn more**: [docs/pro/configuration.md](../docs/pro/configuration.md)
|
|
253
253
|
|
|
254
254
|
---
|
|
255
255
|
|
|
@@ -326,9 +326,9 @@ rails console
|
|
|
326
326
|
|
|
327
327
|
### Next Steps
|
|
328
328
|
|
|
329
|
-
- **Enable caching**: See [docs/caching.md](
|
|
330
|
-
- **Set up Node Renderer**: See [docs/node-renderer/basics.md](
|
|
331
|
-
- **Optimize performance**: See [docs/configuration.md](
|
|
329
|
+
- **Enable caching**: See [docs/pro/caching.md](../docs/pro/caching.md)
|
|
330
|
+
- **Set up Node Renderer**: See [docs/pro/node-renderer/basics.md](../docs/pro/node-renderer/basics.md)
|
|
331
|
+
- **Optimize performance**: See [docs/pro/configuration.md](../docs/pro/configuration.md)
|
|
332
332
|
- **Set up for your team**: See [LICENSE_SETUP.md](./LICENSE_SETUP.md#team-setup)
|
|
333
333
|
|
|
334
334
|
---
|
|
@@ -337,20 +337,20 @@ rails console
|
|
|
337
337
|
|
|
338
338
|
### Installation & Setup
|
|
339
339
|
|
|
340
|
-
- **[Installation Guide](
|
|
340
|
+
- **[Installation Guide](../docs/pro/installation.md)** - Detailed installation instructions
|
|
341
341
|
- **[License Setup](./LICENSE_SETUP.md)** - Complete license configuration guide
|
|
342
|
-
- **[Configuration Reference](
|
|
342
|
+
- **[Configuration Reference](../docs/pro/configuration.md)** - All configuration options
|
|
343
343
|
|
|
344
344
|
### Features
|
|
345
345
|
|
|
346
|
-
- **[Caching Guide](
|
|
347
|
-
- **[Bundle Caching](
|
|
348
|
-
- **[Node Renderer Basics](
|
|
349
|
-
- **[Node Renderer Configuration](
|
|
346
|
+
- **[Caching Guide](../docs/pro/caching.md)** - Fragment and prerender caching
|
|
347
|
+
- **[Bundle Caching](../docs/pro/bundle-caching.md)** - Speed up webpack builds
|
|
348
|
+
- **[Node Renderer Basics](../docs/pro/node-renderer/basics.md)** - Standalone Node.js server
|
|
349
|
+
- **[Node Renderer Configuration](../docs/pro/node-renderer/js-configuration.md)** - JavaScript config
|
|
350
350
|
|
|
351
351
|
### API Reference
|
|
352
352
|
|
|
353
|
-
- **[Ruby API](
|
|
353
|
+
- **[Ruby API](../docs/pro/ruby-api.md)** - Helper methods and utilities
|
|
354
354
|
- **[CHANGELOG](./CHANGELOG.md)** - Version history and upgrade notes
|
|
355
355
|
|
|
356
356
|
### Upgrading
|
|
@@ -408,7 +408,7 @@ Check out these production applications using React on Rails Pro:
|
|
|
408
408
|
|
|
409
409
|
- **📧 Email Support**: [support@shakacode.com](mailto:support@shakacode.com)
|
|
410
410
|
- **💼 Sales & Licensing**: [justin@shakacode.com](mailto:justin@shakacode.com)
|
|
411
|
-
- **📖 Documentation**: [docs/](
|
|
411
|
+
- **📖 Documentation**: [docs/pro/](../docs/pro/)
|
|
412
412
|
- **🐛 Found a Bug?**: Email [support@shakacode.com](mailto:support@shakacode.com) (for Pro customers)
|
|
413
413
|
|
|
414
414
|
### Professional Services
|
|
@@ -314,7 +314,13 @@ module ReactOnRailsProHelper
|
|
|
314
314
|
|
|
315
315
|
# Enqueue remaining chunks asynchronously
|
|
316
316
|
@async_barrier.async do
|
|
317
|
-
rest_chunks.each
|
|
317
|
+
rest_chunks.each do |chunk|
|
|
318
|
+
break if response.stream.closed?
|
|
319
|
+
|
|
320
|
+
@main_output_queue.enqueue(chunk)
|
|
321
|
+
end
|
|
322
|
+
rescue Async::Queue::ClosedError
|
|
323
|
+
# Queue closed due to error/disconnect in another component — stop enqueuing
|
|
318
324
|
end
|
|
319
325
|
|
|
320
326
|
# Return first chunk directly
|
|
@@ -436,8 +442,8 @@ module ReactOnRailsProHelper
|
|
|
436
442
|
# Start an async task on the barrier to stream all chunks
|
|
437
443
|
@async_barrier.async do
|
|
438
444
|
stream = yield
|
|
439
|
-
process_stream_chunks(stream, first_chunk_var, all_chunks)
|
|
440
|
-
on_complete&.call(all_chunks)
|
|
445
|
+
fully_consumed = process_stream_chunks(stream, first_chunk_var, all_chunks)
|
|
446
|
+
on_complete&.call(all_chunks) if fully_consumed
|
|
441
447
|
rescue StandardError => e
|
|
442
448
|
# Propagate the error to the calling fiber via the variable.
|
|
443
449
|
# Async::Variable can only be resolved once — if it was already resolved
|
|
@@ -457,9 +463,9 @@ module ReactOnRailsProHelper
|
|
|
457
463
|
end
|
|
458
464
|
end
|
|
459
465
|
|
|
460
|
-
# Wait for and return the first chunk (blocking)
|
|
461
|
-
|
|
462
|
-
result = first_chunk_var.
|
|
466
|
+
# Wait for and return the first chunk (blocking).
|
|
467
|
+
# Async::Variable#wait blocks until resolved, then returns the stored value.
|
|
468
|
+
result = first_chunk_var.wait
|
|
463
469
|
|
|
464
470
|
# If the async task stored an exception (pre-first-chunk error), raise it now.
|
|
465
471
|
# This happens BEFORE response.stream.write(template_string) in
|
|
@@ -470,12 +476,17 @@ module ReactOnRailsProHelper
|
|
|
470
476
|
result
|
|
471
477
|
end
|
|
472
478
|
|
|
479
|
+
# Returns true if the stream was fully consumed, false if aborted (client disconnect).
|
|
480
|
+
# When false, callers must NOT invoke on_complete to avoid caching partial data.
|
|
473
481
|
def process_stream_chunks(stream, first_chunk_var, all_chunks)
|
|
474
482
|
is_first = true
|
|
475
483
|
|
|
476
484
|
stream.each_chunk do |chunk|
|
|
477
|
-
#
|
|
478
|
-
|
|
485
|
+
# Client disconnected — abort without caching partial results
|
|
486
|
+
if response.stream.closed?
|
|
487
|
+
first_chunk_var.value = nil if is_first
|
|
488
|
+
return false
|
|
489
|
+
end
|
|
479
490
|
|
|
480
491
|
all_chunks&.push(chunk)
|
|
481
492
|
|
|
@@ -491,6 +502,7 @@ module ReactOnRailsProHelper
|
|
|
491
502
|
|
|
492
503
|
# Handle case where stream has no chunks
|
|
493
504
|
first_chunk_var.value = nil if is_first
|
|
505
|
+
true
|
|
494
506
|
end
|
|
495
507
|
|
|
496
508
|
def internal_stream_react_component(component_name, options = {})
|
|
@@ -15,7 +15,22 @@ module ReactOnRailsPro
|
|
|
15
15
|
|
|
16
16
|
stream_view_containing_react_components(
|
|
17
17
|
template: custom_rsc_payload_template,
|
|
18
|
-
layout: false
|
|
18
|
+
layout: false,
|
|
19
|
+
# Render as text so Rails does not inject HTML view annotation comments
|
|
20
|
+
# into the NDJSON stream. Custom template overrides must resolve to a
|
|
21
|
+
# text or format-neutral template, not `.html.erb`.
|
|
22
|
+
formats: [:text],
|
|
23
|
+
content_type: "application/x-ndjson"
|
|
24
|
+
)
|
|
25
|
+
rescue ActionView::MissingTemplate => e
|
|
26
|
+
raise e.exception(
|
|
27
|
+
"[React on Rails Pro] RSC payload templates are now rendered with format :text. " \
|
|
28
|
+
"If you override `custom_rsc_payload_template`, make sure the override resolves to " \
|
|
29
|
+
"a text or format-neutral template (for example `rsc_payload.text.erb`) instead of " \
|
|
30
|
+
"only `.html.erb`. See\n" \
|
|
31
|
+
"https://github.com/shakacode/react_on_rails/blob/master/docs/pro/updating.md " \
|
|
32
|
+
"for upgrade notes.\n\n" \
|
|
33
|
+
"Original error: #{e.message}"
|
|
19
34
|
)
|
|
20
35
|
end
|
|
21
36
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "English"
|
|
4
|
+
|
|
3
5
|
module ReactOnRailsPro
|
|
4
6
|
module Stream
|
|
5
7
|
extend ActiveSupport::Concern
|
|
@@ -12,6 +14,10 @@ module ReactOnRailsPro
|
|
|
12
14
|
#
|
|
13
15
|
# @param template [String] The path to the template file to be streamed.
|
|
14
16
|
# @param close_stream_at_end [Boolean] Whether to automatically close the stream after rendering (default: true).
|
|
17
|
+
# @param content_type [String, nil] Optional response content type. Set after rendering but before the first
|
|
18
|
+
# stream write, overriding any content type inferred from the template format. When using
|
|
19
|
+
# a non-HTML `formats:` value (for example `[:text]`), pass `content_type` too unless
|
|
20
|
+
# committing the format-derived MIME type is intentional.
|
|
15
21
|
# @param render_options [Hash] Additional options to pass to `render_to_string`.
|
|
16
22
|
#
|
|
17
23
|
# components must be added to the view using the `stream_react_component` helper.
|
|
@@ -30,10 +36,13 @@ module ReactOnRailsPro
|
|
|
30
36
|
# For more details, refer to `lib/react_on_rails/helper.rb` in the react_on_rails repository.
|
|
31
37
|
#
|
|
32
38
|
# @see ReactOnRails::Helper#stream_react_component
|
|
33
|
-
def stream_view_containing_react_components(
|
|
39
|
+
def stream_view_containing_react_components(
|
|
40
|
+
template:, close_stream_at_end: true, content_type: nil, **render_options
|
|
41
|
+
)
|
|
34
42
|
require "async"
|
|
35
43
|
require "async/barrier"
|
|
36
44
|
require "async/limited_queue"
|
|
45
|
+
warn_on_non_html_formats_without_content_type(render_options[:formats], content_type)
|
|
37
46
|
|
|
38
47
|
Sync do |parent_task|
|
|
39
48
|
# Initialize async primitives for concurrent component streaming
|
|
@@ -48,6 +57,11 @@ module ReactOnRailsPro
|
|
|
48
57
|
# View may contain extra newlines, chunk already contains a newline
|
|
49
58
|
# Having multiple newlines between chunks causes hydration errors
|
|
50
59
|
# So we strip extra newlines from the template string and add a single newline
|
|
60
|
+
# `formats: [:text]` causes render_to_string to set response.content_type
|
|
61
|
+
# to `text/plain`; override it here before the first stream write, which
|
|
62
|
+
# is when ActionController::Live commits headers. render_to_string itself
|
|
63
|
+
# never writes to response.stream, so this assignment is always safe.
|
|
64
|
+
response.content_type = content_type if content_type
|
|
51
65
|
response.stream.write(template_string)
|
|
52
66
|
|
|
53
67
|
drain_streams_concurrently(parent_task)
|
|
@@ -80,8 +94,6 @@ module ReactOnRailsPro
|
|
|
80
94
|
# - Barrier is stopped to cancel all producer tasks, preventing wasted work
|
|
81
95
|
# - No exception propagates to the controller for client disconnects
|
|
82
96
|
def drain_streams_concurrently(parent_task)
|
|
83
|
-
client_disconnected = false
|
|
84
|
-
|
|
85
97
|
writing_task = parent_task.async do
|
|
86
98
|
# Drain all remaining chunks from the queue to the response stream
|
|
87
99
|
while (chunk = @main_output_queue.dequeue)
|
|
@@ -89,8 +101,13 @@ module ReactOnRailsPro
|
|
|
89
101
|
end
|
|
90
102
|
rescue IOError, Errno::EPIPE => e
|
|
91
103
|
# Client disconnected - stop writing gracefully
|
|
92
|
-
client_disconnected = true
|
|
93
104
|
log_client_disconnect("writer", e)
|
|
105
|
+
ensure
|
|
106
|
+
# Cancel all producers when writer exits for ANY reason (normal completion,
|
|
107
|
+
# client disconnect, or unexpected error). Prevents deadlock where producers
|
|
108
|
+
# block on enqueue to a full queue that nobody is consuming.
|
|
109
|
+
# Idempotent — no-op if barrier tasks already completed.
|
|
110
|
+
@async_barrier.stop
|
|
94
111
|
end
|
|
95
112
|
|
|
96
113
|
# Wait for all component streaming tasks to complete
|
|
@@ -101,16 +118,22 @@ module ReactOnRailsPro
|
|
|
101
118
|
raise e
|
|
102
119
|
end
|
|
103
120
|
ensure
|
|
104
|
-
#
|
|
105
|
-
|
|
121
|
+
# Capture the primary exception (if any) BEFORE any cleanup that could raise.
|
|
122
|
+
# In an ensure block, $ERROR_INFO holds the exception currently propagating
|
|
123
|
+
# out of the method (nil if returning normally). We must snapshot it before
|
|
124
|
+
# the begin/rescue below, where $ERROR_INFO would reflect the caught exception.
|
|
125
|
+
primary_exception = $ERROR_INFO
|
|
106
126
|
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
# writing_task's rescue block sets the flag)
|
|
110
|
-
writing_task.wait
|
|
127
|
+
# Close the queue to unblock writing_task (it may be waiting on dequeue)
|
|
128
|
+
@main_output_queue.close
|
|
111
129
|
|
|
112
|
-
#
|
|
113
|
-
|
|
130
|
+
# Wait for writing_task to finish. Wrap in rescue to avoid masking a primary
|
|
131
|
+
# exception (e.g., producer error) with a secondary writing_task exception.
|
|
132
|
+
begin
|
|
133
|
+
writing_task.wait
|
|
134
|
+
rescue StandardError
|
|
135
|
+
raise unless primary_exception
|
|
136
|
+
end
|
|
114
137
|
end
|
|
115
138
|
|
|
116
139
|
def log_client_disconnect(context, exception)
|
|
@@ -120,5 +143,19 @@ module ReactOnRailsPro
|
|
|
120
143
|
"[React on Rails Pro] Client disconnected during streaming (#{context}): #{exception.class}"
|
|
121
144
|
end
|
|
122
145
|
end
|
|
146
|
+
|
|
147
|
+
def warn_on_non_html_formats_without_content_type(formats, content_type)
|
|
148
|
+
return if content_type.present?
|
|
149
|
+
|
|
150
|
+
requested_formats = Array(formats).compact.map(&:to_sym)
|
|
151
|
+
return if requested_formats.empty? || requested_formats.all?(:html)
|
|
152
|
+
|
|
153
|
+
Rails.logger.warn(
|
|
154
|
+
"[React on Rails Pro] stream_view_containing_react_components received non-HTML formats " \
|
|
155
|
+
"#{requested_formats.inspect} without `content_type:`. Rails will commit the format-derived " \
|
|
156
|
+
"MIME type (for example `text/plain` for `:text`). Pass `content_type:` explicitly when " \
|
|
157
|
+
"streaming non-HTML responses."
|
|
158
|
+
)
|
|
159
|
+
end
|
|
123
160
|
end
|
|
124
161
|
end
|
|
@@ -7,8 +7,10 @@ module ReactOnRailsPro
|
|
|
7
7
|
LICENSE_URL = "https://www.shakacode.com/react-on-rails-pro/"
|
|
8
8
|
# TODO: Remove this legacy migration warning path after 16.5.0 stable release (target: 2026-05-31).
|
|
9
9
|
LEGACY_LICENSE_FILE = "config/react_on_rails_pro_license.key"
|
|
10
|
+
RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS = ["Rack::Deflater"].freeze
|
|
10
11
|
private_constant :LICENSE_URL
|
|
11
12
|
private_constant :LEGACY_LICENSE_FILE
|
|
13
|
+
private_constant :RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS
|
|
12
14
|
|
|
13
15
|
initializer "react_on_rails_pro.routes" do
|
|
14
16
|
ActionDispatch::Routing::Mapper.include ReactOnRailsPro::Routes
|
|
@@ -20,6 +22,10 @@ module ReactOnRailsPro
|
|
|
20
22
|
config.after_initialize { ReactOnRailsPro::Engine.log_license_status }
|
|
21
23
|
end
|
|
22
24
|
|
|
25
|
+
initializer "react_on_rails_pro.check_rsc_streaming_middleware" do
|
|
26
|
+
config.after_initialize { ReactOnRailsPro::Engine.log_rsc_streaming_middleware_warning }
|
|
27
|
+
end
|
|
28
|
+
|
|
23
29
|
class << self
|
|
24
30
|
def log_license_status
|
|
25
31
|
status = ReactOnRailsPro::LicenseValidator.license_status
|
|
@@ -40,6 +46,24 @@ module ReactOnRailsPro
|
|
|
40
46
|
end
|
|
41
47
|
end
|
|
42
48
|
|
|
49
|
+
def log_rsc_streaming_middleware_warning
|
|
50
|
+
return unless ReactOnRailsPro.configuration.enable_rsc_support
|
|
51
|
+
return if Rails.env.test?
|
|
52
|
+
|
|
53
|
+
middleware_names = middleware_stack_names
|
|
54
|
+
problematic = RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS & middleware_names
|
|
55
|
+
return if problematic.empty?
|
|
56
|
+
|
|
57
|
+
route_path = ReactOnRailsPro.configuration.rsc_payload_generation_url_path
|
|
58
|
+
Rails.logger.warn(
|
|
59
|
+
"[React on Rails Pro] React Server Components support is enabled and the middleware " \
|
|
60
|
+
"stack includes #{problematic.join(', ')}. Compression and other response-transforming " \
|
|
61
|
+
"middleware can interfere with ActionController::Live NDJSON streaming. If your " \
|
|
62
|
+
"`#{route_path}` payload route is not already exempt, consider bypassing " \
|
|
63
|
+
"#{problematic.join(', ')} for that endpoint if you see stalled or corrupted RSC payloads."
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
43
67
|
private
|
|
44
68
|
|
|
45
69
|
def log_valid_license
|
|
@@ -105,6 +129,40 @@ module ReactOnRailsPro
|
|
|
105
129
|
Rails.logger.info message
|
|
106
130
|
end
|
|
107
131
|
end
|
|
132
|
+
|
|
133
|
+
def middleware_stack_names
|
|
134
|
+
middleware_stack = Rails.application&.middleware
|
|
135
|
+
return [] unless middleware_stack
|
|
136
|
+
|
|
137
|
+
entries =
|
|
138
|
+
if middleware_stack.respond_to?(:middlewares)
|
|
139
|
+
middleware_stack.middlewares
|
|
140
|
+
elsif middleware_stack.respond_to?(:to_a)
|
|
141
|
+
middleware_stack.to_a
|
|
142
|
+
else
|
|
143
|
+
Array(middleware_stack)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
entries.filter_map { |entry| middleware_entry_name(entry) }.uniq
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def middleware_entry_name(entry)
|
|
150
|
+
candidate =
|
|
151
|
+
if entry.respond_to?(:klass) && entry.klass
|
|
152
|
+
entry.klass
|
|
153
|
+
elsif entry.is_a?(Array)
|
|
154
|
+
entry.first
|
|
155
|
+
else
|
|
156
|
+
entry
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
case candidate
|
|
160
|
+
when Module
|
|
161
|
+
candidate.name
|
|
162
|
+
else
|
|
163
|
+
candidate.to_s.presence
|
|
164
|
+
end
|
|
165
|
+
end
|
|
108
166
|
end
|
|
109
167
|
end
|
|
110
168
|
end
|
|
@@ -319,6 +319,16 @@ module ReactOnRailsPro
|
|
|
319
319
|
end
|
|
320
320
|
|
|
321
321
|
response = HTTPX.get(path)
|
|
322
|
+
error = response.error
|
|
323
|
+
if error
|
|
324
|
+
# Re-raise via rescue so Ruby sets error.cause for exception chaining.
|
|
325
|
+
begin
|
|
326
|
+
raise error
|
|
327
|
+
rescue StandardError
|
|
328
|
+
raise ReactOnRailsPro::Error, "Failed to fetch dev-server asset from #{path}: #{error}"
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
322
332
|
response.body
|
|
323
333
|
else
|
|
324
334
|
Pathname.new(path)
|
|
@@ -113,7 +113,7 @@ module ReactOnRailsPro
|
|
|
113
113
|
|
|
114
114
|
def process_response_chunks(stream_response, error_body)
|
|
115
115
|
loop_response_lines(stream_response) do |chunk|
|
|
116
|
-
if
|
|
116
|
+
if response_has_error_status?(stream_response)
|
|
117
117
|
error_body << chunk
|
|
118
118
|
next
|
|
119
119
|
end
|
|
@@ -123,6 +123,15 @@ module ReactOnRailsPro
|
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
def response_has_error_status?(response)
|
|
127
|
+
return true if response.is_a?(HTTPX::ErrorResponse)
|
|
128
|
+
|
|
129
|
+
response.status >= 400
|
|
130
|
+
rescue NoMethodError
|
|
131
|
+
# HTTPX::StreamResponse can fail to delegate #status for non-streaming errors.
|
|
132
|
+
true
|
|
133
|
+
end
|
|
134
|
+
|
|
126
135
|
def handle_http_error(error, error_body, send_bundle)
|
|
127
136
|
response = error.response
|
|
128
137
|
case response.status
|