react_on_rails_pro 17.1.0.rc.1 → 17.1.0.rc.2
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af819b8f0320a216aaa75d812fa78f376694161a3225932570950158241bced4
|
|
4
|
+
data.tar.gz: b3d8ce02b32b91360b89b2278d05a5e5ba5716cbd7a5a909e2c000e89e0f93d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ffdefae73ac6d2f573b4d2041c81484356d6aaf17968e1a33187b6fefdefd4017eda37a6471e6bdd2dbac82f6ed64ba85a7e0fee11208513137b439712c5af58
|
|
7
|
+
data.tar.gz: 3f53042c50b650ef0407db5a763a8378caa2ff03643f3047c690a4c601ed6d1b027ee150614ae903027ead086ff677d2b44b1a5c5715f4a6846718e749efe985
|
data/Gemfile.lock
CHANGED
|
@@ -9,7 +9,7 @@ GIT
|
|
|
9
9
|
PATH
|
|
10
10
|
remote: ..
|
|
11
11
|
specs:
|
|
12
|
-
react_on_rails (17.1.0.rc.
|
|
12
|
+
react_on_rails (17.1.0.rc.2)
|
|
13
13
|
addressable
|
|
14
14
|
connection_pool
|
|
15
15
|
execjs (~> 2.5)
|
|
@@ -20,14 +20,14 @@ PATH
|
|
|
20
20
|
PATH
|
|
21
21
|
remote: .
|
|
22
22
|
specs:
|
|
23
|
-
react_on_rails_pro (17.1.0.rc.
|
|
23
|
+
react_on_rails_pro (17.1.0.rc.2)
|
|
24
24
|
async (>= 2.29)
|
|
25
25
|
async-http (~> 0.95)
|
|
26
26
|
execjs (~> 2.9)
|
|
27
27
|
io-endpoint (~> 0.17.0)
|
|
28
28
|
jwt (>= 2.5, < 4)
|
|
29
29
|
nokogiri (>= 1.12, < 2)
|
|
30
|
-
react_on_rails (= 17.1.0.rc.
|
|
30
|
+
react_on_rails (= 17.1.0.rc.2)
|
|
31
31
|
|
|
32
32
|
GEM
|
|
33
33
|
remote: https://rubygems.org/
|
|
@@ -95,16 +95,24 @@ module ReactOnRailsPro
|
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
def render_streaming_with_cache(prerender_cache_key, js_code, render_options)
|
|
98
|
-
# Streaming path: try to serve from cache; otherwise wrap upstream stream
|
|
98
|
+
# Streaming path: try to serve from cache; otherwise wrap upstream stream.
|
|
99
|
+
# The cache key ignores random dom ids, so a cached stream may have been produced for
|
|
100
|
+
# another mount point; StreamCache rebinds it to this render's dom id (issue #4984).
|
|
99
101
|
cache_options = render_options.internal_option(:cache_options)
|
|
100
|
-
|
|
102
|
+
dom_node_id = render_options.dom_id
|
|
103
|
+
cached_stream = ReactOnRailsPro::StreamCache.fetch_stream(
|
|
104
|
+
prerender_cache_key,
|
|
105
|
+
cache_options:,
|
|
106
|
+
dom_node_id:
|
|
107
|
+
)
|
|
101
108
|
return cached_stream if cached_stream
|
|
102
109
|
|
|
103
110
|
upstream = render_on_pool(js_code, render_options)
|
|
104
111
|
ReactOnRailsPro::StreamCache.wrap_and_cache(
|
|
105
112
|
prerender_cache_key,
|
|
106
113
|
upstream,
|
|
107
|
-
cache_options
|
|
114
|
+
cache_options:,
|
|
115
|
+
dom_node_id:
|
|
108
116
|
)
|
|
109
117
|
end
|
|
110
118
|
|
|
@@ -15,22 +15,38 @@
|
|
|
15
15
|
|
|
16
16
|
module ReactOnRailsPro
|
|
17
17
|
class StreamCache
|
|
18
|
+
CACHED_CHUNKS_KEY = "chunks"
|
|
19
|
+
CACHED_DOM_NODE_ID_KEY = "dom_node_id"
|
|
20
|
+
|
|
18
21
|
class << self
|
|
19
22
|
# Returns a stream-like object that responds to `each_chunk` and yields cached chunks
|
|
20
23
|
# or nil if not present in cache. Pass the same cache_options given to wrap_and_cache
|
|
21
24
|
# so key-altering options such as :namespace resolve to the written entry.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
#
|
|
26
|
+
# `dom_node_id` is the mount point of the render being served. The prerender cache key
|
|
27
|
+
# deliberately ignores random dom ids (see ProRendering.without_random_values) so one
|
|
28
|
+
# cached render serves every mount point, but the cached chunks embed the dom id of the
|
|
29
|
+
# render that produced them (RSC payload keys, console replay scripts). Replaying them
|
|
30
|
+
# unchanged under another mount id leaves the browser without an embedded payload for
|
|
31
|
+
# its own node, so cached chunks are rebound to the current dom id on the way out.
|
|
32
|
+
# See https://github.com/shakacode/react_on_rails/issues/4984.
|
|
33
|
+
def fetch_stream(cache_key, cache_options: nil, dom_node_id: nil)
|
|
34
|
+
entry = cached_entry(Rails.cache.read(cache_key, cache_options))
|
|
35
|
+
return nil unless entry
|
|
25
36
|
|
|
26
|
-
|
|
37
|
+
chunks = DomNodeIdRewriter.rewrite(
|
|
38
|
+
entry.fetch(CACHED_CHUNKS_KEY),
|
|
39
|
+
from: entry[CACHED_DOM_NODE_ID_KEY],
|
|
40
|
+
to: dom_node_id
|
|
41
|
+
)
|
|
42
|
+
build_stream_from_chunks(chunks)
|
|
27
43
|
end
|
|
28
44
|
|
|
29
45
|
# Wraps an upstream stream (responds to `each_chunk`), yields chunks downstream while
|
|
30
|
-
# buffering them, and writes the chunks array to Rails.cache on
|
|
31
|
-
# Returns a stream-like object that responds to `each_chunk`.
|
|
32
|
-
def wrap_and_cache(cache_key, upstream_stream, cache_options: nil)
|
|
33
|
-
component = CachingComponent.new(upstream_stream, cache_key, cache_options)
|
|
46
|
+
# buffering them, and writes the chunks array plus the producing dom id to Rails.cache on
|
|
47
|
+
# successful completion. Returns a stream-like object that responds to `each_chunk`.
|
|
48
|
+
def wrap_and_cache(cache_key, upstream_stream, cache_options: nil, dom_node_id: nil)
|
|
49
|
+
component = CachingComponent.new(upstream_stream, cache_key, cache_options, dom_node_id)
|
|
34
50
|
ReactOnRailsPro::StreamDecorator.new(component)
|
|
35
51
|
end
|
|
36
52
|
|
|
@@ -39,6 +55,97 @@ module ReactOnRailsPro
|
|
|
39
55
|
component = CachedChunksComponent.new(chunks)
|
|
40
56
|
ReactOnRailsPro::StreamDecorator.new(component)
|
|
41
57
|
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Entries written before the producing dom id was retained were bare chunk arrays. They
|
|
62
|
+
# cannot be rebound safely, so they are treated as a miss and re-rendered into the current
|
|
63
|
+
# shape. (The base cache key also embeds the Pro version, so such entries are not normally
|
|
64
|
+
# reachable after an upgrade.)
|
|
65
|
+
def cached_entry(cached)
|
|
66
|
+
return nil unless cached.is_a?(Hash) && cached[CACHED_CHUNKS_KEY].is_a?(Array)
|
|
67
|
+
|
|
68
|
+
cached
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Rebinds the dom id embedded in cached chunks to the mount point of the current render.
|
|
73
|
+
# Chunks are either Strings or renderer Hashes whose "html" and "consoleReplayScript" values
|
|
74
|
+
# carry the markup. The html is rewritten as one document and re-split at the original chunk
|
|
75
|
+
# boundaries, so an id that straddles two chunks is still replaced and the frame count is kept.
|
|
76
|
+
module DomNodeIdRewriter
|
|
77
|
+
HTML_KEY = "html"
|
|
78
|
+
CONSOLE_REPLAY_SCRIPT_KEY = "consoleReplayScript"
|
|
79
|
+
|
|
80
|
+
module_function
|
|
81
|
+
|
|
82
|
+
def rewrite(chunks, from:, to:)
|
|
83
|
+
return chunks if from.to_s.empty? || to.to_s.empty? || from == to
|
|
84
|
+
|
|
85
|
+
html_pieces = chunks.map { |chunk| markup_of(chunk) }
|
|
86
|
+
rewritten_html = resplit(
|
|
87
|
+
html_pieces.join, html_pieces.map(&:bytesize), from, to,
|
|
88
|
+
landing_index: chunks.index { |chunk| markup?(chunk) } || 0
|
|
89
|
+
)
|
|
90
|
+
chunks.each_with_index.map { |chunk, index| rebind_chunk(chunk, rewritten_html[index], from, to) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def markup?(chunk)
|
|
94
|
+
chunk.is_a?(String) || (chunk.is_a?(Hash) && chunk[HTML_KEY].is_a?(String))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Every chunk contributes its markup to one joined document: a String chunk is the markup
|
|
98
|
+
# itself, a Hash chunk contributes its "html", anything else contributes nothing.
|
|
99
|
+
def markup_of(chunk)
|
|
100
|
+
return chunk if chunk.is_a?(String)
|
|
101
|
+
return chunk[HTML_KEY] if chunk.is_a?(Hash) && chunk[HTML_KEY].is_a?(String)
|
|
102
|
+
|
|
103
|
+
""
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def rebind_chunk(chunk, rewritten_html, from, to)
|
|
107
|
+
return rewritten_html if chunk.is_a?(String)
|
|
108
|
+
return chunk unless chunk.is_a?(Hash)
|
|
109
|
+
|
|
110
|
+
rebind_hash(chunk, rewritten_html, from, to)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def rebind_hash(chunk, rewritten_html, from, to)
|
|
114
|
+
rebound = chunk.dup
|
|
115
|
+
rebound[HTML_KEY] = rewritten_html if chunk[HTML_KEY].is_a?(String)
|
|
116
|
+
if chunk[CONSOLE_REPLAY_SCRIPT_KEY].is_a?(String)
|
|
117
|
+
rebound[CONSOLE_REPLAY_SCRIPT_KEY] = replace_literal(chunk[CONSOLE_REPLAY_SCRIPT_KEY], from, to)
|
|
118
|
+
end
|
|
119
|
+
rebound
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Splits `document` back into pieces sized like `sizes` (in bytes). Random dom ids share one
|
|
123
|
+
# format, so the substitution keeps every byte offset and the original boundaries are exact.
|
|
124
|
+
# If the ids ever differ in length, byte offsets would no longer align (and could cut a
|
|
125
|
+
# multibyte character), so the whole rewritten document is delivered in the first piece that
|
|
126
|
+
# carries markup (`landing_index`) and every other piece is empty; the concatenation is
|
|
127
|
+
# identical either way.
|
|
128
|
+
def resplit(document, sizes, from, to, landing_index: 0)
|
|
129
|
+
rewritten = replace_literal(document, from, to)
|
|
130
|
+
if from.bytesize != to.bytesize
|
|
131
|
+
pieces = Array.new(sizes.length, "")
|
|
132
|
+
pieces[landing_index] = rewritten unless pieces.empty?
|
|
133
|
+
return pieces
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
offset = 0
|
|
137
|
+
sizes.each_with_index.map do |size, index|
|
|
138
|
+
piece = index == sizes.length - 1 ? rewritten.byteslice(offset..) : rewritten.byteslice(offset, size)
|
|
139
|
+
offset += size
|
|
140
|
+
piece || ""
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The block form keeps `to` literal: a String replacement would interpret backreference
|
|
145
|
+
# sequences such as `\0` or `\\`.
|
|
146
|
+
def replace_literal(text, from, to)
|
|
147
|
+
text.gsub(from) { to }
|
|
148
|
+
end
|
|
42
149
|
end
|
|
43
150
|
|
|
44
151
|
class CachedChunksComponent
|
|
@@ -54,10 +161,11 @@ module ReactOnRailsPro
|
|
|
54
161
|
end
|
|
55
162
|
|
|
56
163
|
class CachingComponent
|
|
57
|
-
def initialize(upstream_stream, cache_key, cache_options)
|
|
164
|
+
def initialize(upstream_stream, cache_key, cache_options, dom_node_id = nil)
|
|
58
165
|
@upstream_stream = upstream_stream
|
|
59
166
|
@cache_key = cache_key
|
|
60
167
|
@cache_options = cache_options
|
|
168
|
+
@dom_node_id = dom_node_id
|
|
61
169
|
end
|
|
62
170
|
|
|
63
171
|
def each_chunk(&block)
|
|
@@ -85,7 +193,11 @@ module ReactOnRailsPro
|
|
|
85
193
|
# See https://github.com/shakacode/react_on_rails/issues/4581.
|
|
86
194
|
return if stream_has_errors
|
|
87
195
|
|
|
88
|
-
Rails.cache.write(
|
|
196
|
+
Rails.cache.write(
|
|
197
|
+
@cache_key,
|
|
198
|
+
{ CACHED_DOM_NODE_ID_KEY => @dom_node_id, CACHED_CHUNKS_KEY => buffered_chunks },
|
|
199
|
+
@cache_options || {}
|
|
200
|
+
)
|
|
89
201
|
end
|
|
90
202
|
|
|
91
203
|
private
|
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.1.0.rc.
|
|
4
|
+
version: 17.1.0.rc.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Gordon
|
|
@@ -111,14 +111,14 @@ dependencies:
|
|
|
111
111
|
requirements:
|
|
112
112
|
- - '='
|
|
113
113
|
- !ruby/object:Gem::Version
|
|
114
|
-
version: 17.1.0.rc.
|
|
114
|
+
version: 17.1.0.rc.2
|
|
115
115
|
type: :runtime
|
|
116
116
|
prerelease: false
|
|
117
117
|
version_requirements: !ruby/object:Gem::Requirement
|
|
118
118
|
requirements:
|
|
119
119
|
- - '='
|
|
120
120
|
- !ruby/object:Gem::Version
|
|
121
|
-
version: 17.1.0.rc.
|
|
121
|
+
version: 17.1.0.rc.2
|
|
122
122
|
- !ruby/object:Gem::Dependency
|
|
123
123
|
name: bundler
|
|
124
124
|
requirement: !ruby/object:Gem::Requirement
|