inertia_hanami 0.1.0
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 +7 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +18 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/PLAN.md +193 -0
- data/README.md +417 -0
- data/Rakefile +16 -0
- data/lib/generators/inertia_hanami/install_generator.rb +216 -0
- data/lib/generators/inertia_hanami/templates/helpers.rb.erb +12 -0
- data/lib/generators/inertia_hanami/templates/layout.html.erb.erb +15 -0
- data/lib/generators/inertia_hanami/templates/provider.rb.erb +5 -0
- data/lib/generators/inertia_hanami/templates/sample_action.rb.erb +15 -0
- data/lib/generators/inertia_hanami/templates/sample_template.html.erb.erb +1 -0
- data/lib/generators/inertia_hanami/templates/sample_view.rb.erb +13 -0
- data/lib/inertia_hanami/action.rb +214 -0
- data/lib/inertia_hanami/asset_version.rb +23 -0
- data/lib/inertia_hanami/cli/commands/install.rb +29 -0
- data/lib/inertia_hanami/cli.rb +16 -0
- data/lib/inertia_hanami/configuration.rb +22 -0
- data/lib/inertia_hanami/helper.rb +50 -0
- data/lib/inertia_hanami/middleware/csrf.rb +63 -0
- data/lib/inertia_hanami/middleware/redirects.rb +67 -0
- data/lib/inertia_hanami/middleware/version.rb +37 -0
- data/lib/inertia_hanami/prop_evaluator.rb +32 -0
- data/lib/inertia_hanami/props.rb +68 -0
- data/lib/inertia_hanami/protocol_builder.rb +194 -0
- data/lib/inertia_hanami/provider.rb +33 -0
- data/lib/inertia_hanami/renderer.rb +92 -0
- data/lib/inertia_hanami/request_context.rb +75 -0
- data/lib/inertia_hanami/ssr_renderer.rb +66 -0
- data/lib/inertia_hanami/testing/rspec.rb +265 -0
- data/lib/inertia_hanami/version.rb +5 -0
- data/lib/inertia_hanami.rb +24 -0
- data/sig/inertia_hanami.rbs +4 -0
- metadata +190 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InertiaHanami
|
|
4
|
+
# Decides which props actually go into an Inertia response: applies the
|
|
5
|
+
# partial-reload algorithm (only/except/reset dot-paths), the AlwaysProp
|
|
6
|
+
# bypass, the Optional/Defer exclusion-by-default rule, and Once-prop
|
|
7
|
+
# caching, then collects the deferredProps/mergeProps/prependProps/
|
|
8
|
+
# deepMergeProps/matchPropsOn/onceProps/scrollProps metadata the client
|
|
9
|
+
# needs.
|
|
10
|
+
#
|
|
11
|
+
# Operates on the already-evaluated output of PropEvaluator (a Hash whose
|
|
12
|
+
# leaves are plain values or Props::Base instances with resolved,
|
|
13
|
+
# argument-free blocks) plus plain Ruby values for the partial-reload
|
|
14
|
+
# headers, so it stays framework/header-parsing free.
|
|
15
|
+
#
|
|
16
|
+
# Ported from inertia-rails' props_resolver.rb algorithm (inertia-rage's
|
|
17
|
+
# protocol_builder.rb, the originally intended porting source, isn't
|
|
18
|
+
# available as a dependency here), adapted to this repo's Data-based Props
|
|
19
|
+
# wrappers.
|
|
20
|
+
# rubocop:disable Metrics/ClassLength -- one small dedicated transform_* method per prop type
|
|
21
|
+
class ProtocolBuilder
|
|
22
|
+
# Sentinel returned by #transform to signal a filtered-out prop, so a
|
|
23
|
+
# Hash node can tell "excluded" apart from a legitimately nil value.
|
|
24
|
+
DROP = Object.new.freeze
|
|
25
|
+
private_constant :DROP
|
|
26
|
+
|
|
27
|
+
# `partial` bundles the X-Inertia-Partial-* / X-Inertia-Reset /
|
|
28
|
+
# X-Inertia-Except-Once-Props / X-Inertia-Infinite-Scroll-Merge-Intent
|
|
29
|
+
# header values (already split into arrays by the caller, except
|
|
30
|
+
# :scroll_intent which is a single raw value): :component, :only,
|
|
31
|
+
# :except, :reset, :except_once, :scroll_intent.
|
|
32
|
+
def initialize(component:, props:, partial: {})
|
|
33
|
+
@component = component
|
|
34
|
+
@props = props
|
|
35
|
+
assign_partial(partial)
|
|
36
|
+
@deferred_props = Hash.new { |hash, key| hash[key] = [] }
|
|
37
|
+
@merge_props = []
|
|
38
|
+
@prepend_props = []
|
|
39
|
+
@deep_merge_props = []
|
|
40
|
+
@match_props_on = []
|
|
41
|
+
@once_props = {}
|
|
42
|
+
@scroll_props = {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def call
|
|
46
|
+
{
|
|
47
|
+
props: transform(@props, []),
|
|
48
|
+
deferredProps: presence(@deferred_props.transform_values(&:sort)),
|
|
49
|
+
mergeProps: presence(@merge_props),
|
|
50
|
+
prependProps: presence(@prepend_props),
|
|
51
|
+
deepMergeProps: presence(@deep_merge_props),
|
|
52
|
+
matchPropsOn: presence(@match_props_on),
|
|
53
|
+
onceProps: presence(@once_props),
|
|
54
|
+
scrollProps: presence(@scroll_props)
|
|
55
|
+
}.compact
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def assign_partial(partial)
|
|
61
|
+
@partial = !partial[:component].nil? && partial[:component] == @component
|
|
62
|
+
@only = partial[:only] || []
|
|
63
|
+
@except = partial[:except] || []
|
|
64
|
+
@reset = partial[:reset] || []
|
|
65
|
+
@except_once = partial[:except_once] || []
|
|
66
|
+
@scroll_intent = partial[:scroll_intent] || "append"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Maps a node's class to the transform method that handles it, so
|
|
70
|
+
# #transform stays a flat dispatch instead of an if/elsif chain.
|
|
71
|
+
TRANSFORMERS = {
|
|
72
|
+
Props::Once => :transform_once,
|
|
73
|
+
Props::Scroll => :transform_scroll,
|
|
74
|
+
Props::Merge => :transform_merge,
|
|
75
|
+
Props::Defer => :transform_defer,
|
|
76
|
+
Props::Optional => :transform_optional
|
|
77
|
+
}.freeze
|
|
78
|
+
private_constant :TRANSFORMERS
|
|
79
|
+
|
|
80
|
+
def transform(node, path)
|
|
81
|
+
return transform_hash(node, path) if node.is_a?(Hash)
|
|
82
|
+
return node.resolve if node.is_a?(Props::Always)
|
|
83
|
+
|
|
84
|
+
transformer = TRANSFORMERS[node.class]
|
|
85
|
+
return send(transformer, node, path) if transformer
|
|
86
|
+
|
|
87
|
+
transform_plain(node, path)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def transform_hash(node, path)
|
|
91
|
+
node.each_with_object({}) do |(key, value), acc|
|
|
92
|
+
resolved = transform(value, path + [key.to_s])
|
|
93
|
+
acc[key] = resolved unless resolved == DROP
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def transform_plain(node, path)
|
|
98
|
+
keep_prop?(path) ? node : DROP
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def transform_optional(node, path)
|
|
102
|
+
keep_default_excluded?(path) ? node.resolve : DROP
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def transform_defer(node, path)
|
|
106
|
+
return node.resolve if keep_default_excluded?(path)
|
|
107
|
+
|
|
108
|
+
@deferred_props[node.group] << path.join(".")
|
|
109
|
+
DROP
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def transform_merge(node, path)
|
|
113
|
+
return DROP unless keep_prop?(path)
|
|
114
|
+
|
|
115
|
+
dot_path = path.join(".")
|
|
116
|
+
(node.deep_merge ? @deep_merge_props : @merge_props) << dot_path
|
|
117
|
+
@match_props_on << "#{dot_path}.#{node.match_on}" if node.match_on
|
|
118
|
+
node.resolve
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def transform_once(node, path)
|
|
122
|
+
dot_path = path.join(".")
|
|
123
|
+
key = node.key || dot_path
|
|
124
|
+
return DROP if once_cache_hit?(node, dot_path, key)
|
|
125
|
+
return DROP unless keep_prop?(path)
|
|
126
|
+
|
|
127
|
+
@once_props[key] = { "prop" => dot_path, "expiresAt" => node.expires_at }.compact
|
|
128
|
+
node.resolve
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def once_cache_hit?(node, dot_path, key)
|
|
132
|
+
reset_requested = @reset.include?(dot_path) || @reset.include?(key)
|
|
133
|
+
already_cached = !node.fresh && (@except_once.include?(key) || @except_once.include?(dot_path))
|
|
134
|
+
!reset_requested && already_cached
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def transform_scroll(node, path)
|
|
138
|
+
return DROP unless keep_prop?(path)
|
|
139
|
+
|
|
140
|
+
dot_path = path.join(".")
|
|
141
|
+
(@scroll_intent == "prepend" ? @prepend_props : @merge_props) << dot_path
|
|
142
|
+
@match_props_on << "#{dot_path}.#{node.match_on}" if node.match_on
|
|
143
|
+
@scroll_props[dot_path] = scroll_metadata(node, dot_path)
|
|
144
|
+
node.resolve
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def scroll_metadata(node, dot_path)
|
|
148
|
+
{
|
|
149
|
+
"pageName" => node.page_name,
|
|
150
|
+
"previousPage" => node.previous_page,
|
|
151
|
+
"nextPage" => node.next_page,
|
|
152
|
+
"currentPage" => node.current_page,
|
|
153
|
+
"reset" => @reset.include?(dot_path)
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Optional/Defer: only included when a partial reload explicitly names
|
|
158
|
+
# them via `only`, subject to `except` on top.
|
|
159
|
+
def keep_default_excluded?(path)
|
|
160
|
+
return false unless @partial
|
|
161
|
+
return false if @only.empty? || !path_matches?(path, @only)
|
|
162
|
+
|
|
163
|
+
!excluded_by_except?(path)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Plain values/Merge/Once: excluded by `except`, or by a non-empty
|
|
167
|
+
# `only` during a partial reload that doesn't match this path.
|
|
168
|
+
def keep_prop?(path)
|
|
169
|
+
return false if excluded_by_except?(path)
|
|
170
|
+
return true unless @partial && !@only.empty?
|
|
171
|
+
|
|
172
|
+
path_matches?(path, @only)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def excluded_by_except?(path)
|
|
176
|
+
@partial && !@except.empty? && path_matches?(path, @except)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A dot-path matches a set entry if it's equal to it, a descendant of it
|
|
180
|
+
# (the entry names an ancestor group), or an ancestor of it (walking
|
|
181
|
+
# through an intermediate node on the way to a deeper match).
|
|
182
|
+
def path_matches?(path, set)
|
|
183
|
+
dot_path = path.join(".")
|
|
184
|
+
set.any? do |entry|
|
|
185
|
+
entry == dot_path || dot_path.start_with?("#{entry}.") || entry.start_with?("#{dot_path}.")
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def presence(collection)
|
|
190
|
+
collection.nil? || collection.empty? ? nil : collection
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
# rubocop:enable Metrics/ClassLength
|
|
194
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hanami"
|
|
4
|
+
require "dry/system"
|
|
5
|
+
require "hanami/provider/source"
|
|
6
|
+
|
|
7
|
+
module InertiaHanami
|
|
8
|
+
class Provider < Hanami::Provider::Source
|
|
9
|
+
def prepare
|
|
10
|
+
require "inertia_hanami/configuration"
|
|
11
|
+
require "inertia_hanami/asset_version"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def start
|
|
15
|
+
configuration = InertiaHanami::Configuration.new
|
|
16
|
+
|
|
17
|
+
if configuration.config.version.nil?
|
|
18
|
+
configuration.config.version = InertiaHanami::AssetVersion.digest(assets_root)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
register("config", configuration.config)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def assets_root
|
|
27
|
+
return nil unless target.container.providers.key?(:assets)
|
|
28
|
+
|
|
29
|
+
target.start(:assets)
|
|
30
|
+
target[:assets].root
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module InertiaHanami
|
|
6
|
+
# Builds the Inertia page envelope and decides whether to answer an
|
|
7
|
+
# Inertia XHR request directly (JSON body) or to hand the page off to
|
|
8
|
+
# Hanami's normal rendering pipeline for the initial full-page load
|
|
9
|
+
# (HTML, via the layout's `inertia_root(page:)` helper).
|
|
10
|
+
#
|
|
11
|
+
# Expects `props` to already be evaluated (see PropEvaluator) - this class
|
|
12
|
+
# only applies the partial-reload/prop-filtering algorithm (via
|
|
13
|
+
# ProtocolBuilder) and shapes the resulting envelope.
|
|
14
|
+
class Renderer
|
|
15
|
+
def initialize(request:, response:, component:, props: {}, url: nil, version: nil,
|
|
16
|
+
encrypt_history: false, clear_history: false)
|
|
17
|
+
@request = request
|
|
18
|
+
@response = response
|
|
19
|
+
@component = component
|
|
20
|
+
@props = props
|
|
21
|
+
@url = url || request.fullpath
|
|
22
|
+
@version = version
|
|
23
|
+
@encrypt_history = encrypt_history
|
|
24
|
+
@clear_history = clear_history
|
|
25
|
+
@request_context = RequestContext.new(request.env)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render
|
|
29
|
+
request_context.inertia? ? render_inertia_response : render_initial_load
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :request_context
|
|
35
|
+
|
|
36
|
+
def render_inertia_response
|
|
37
|
+
@response.headers["X-Inertia"] = "true"
|
|
38
|
+
@response.format = :json
|
|
39
|
+
@response.body = page.to_json
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_initial_load
|
|
43
|
+
if ssr_enabled?
|
|
44
|
+
result = SSRRenderer.instance.call(page)
|
|
45
|
+
if result
|
|
46
|
+
@response[:ssr_head] = result.head
|
|
47
|
+
@response[:ssr_body] = result.body
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@response[:page] = page
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ssr_enabled?
|
|
56
|
+
Hanami.app["inertia.config"].ssr.enabled
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def page
|
|
60
|
+
@page ||= build_page
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_page
|
|
64
|
+
resolved = ProtocolBuilder.new(
|
|
65
|
+
component: @component,
|
|
66
|
+
props: @props,
|
|
67
|
+
partial: request_context.partial_params
|
|
68
|
+
).call
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
"component" => @component,
|
|
72
|
+
"props" => resolved[:props],
|
|
73
|
+
"url" => @url,
|
|
74
|
+
"version" => @version,
|
|
75
|
+
"encryptHistory" => @encrypt_history,
|
|
76
|
+
"clearHistory" => @clear_history
|
|
77
|
+
}.merge(metadata(resolved))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def metadata(resolved)
|
|
81
|
+
{
|
|
82
|
+
"deferredProps" => resolved[:deferredProps],
|
|
83
|
+
"mergeProps" => resolved[:mergeProps],
|
|
84
|
+
"prependProps" => resolved[:prependProps],
|
|
85
|
+
"deepMergeProps" => resolved[:deepMergeProps],
|
|
86
|
+
"matchPropsOn" => resolved[:matchPropsOn],
|
|
87
|
+
"onceProps" => resolved[:onceProps],
|
|
88
|
+
"scrollProps" => resolved[:scrollProps]
|
|
89
|
+
}.compact
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module InertiaHanami
|
|
4
|
+
# Parses the Inertia protocol's request headers off a Rack env: detection
|
|
5
|
+
# (X-Inertia), the asset version the client has cached (X-Inertia-Version),
|
|
6
|
+
# the partial-reload headers (X-Inertia-Partial-Component,
|
|
7
|
+
# X-Inertia-Partial-Data, X-Inertia-Partial-Except, X-Inertia-Reset,
|
|
8
|
+
# X-Inertia-Except-Once-Props), and the infinite-scroll merge-intent header
|
|
9
|
+
# (X-Inertia-Infinite-Scroll-Merge-Intent).
|
|
10
|
+
#
|
|
11
|
+
# Stays framework/Hanami-request free (plain Rack env in, plain values out)
|
|
12
|
+
# so it can be constructed from anywhere a Rack env is available.
|
|
13
|
+
class RequestContext
|
|
14
|
+
def initialize(env)
|
|
15
|
+
@env = env
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def inertia?
|
|
19
|
+
@env["HTTP_X_INERTIA"] == "true"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def version
|
|
23
|
+
@env["HTTP_X_INERTIA_VERSION"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def partial_component
|
|
27
|
+
@env["HTTP_X_INERTIA_PARTIAL_COMPONENT"]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def partial?
|
|
31
|
+
!partial_component.nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def partial_only
|
|
35
|
+
split_header("HTTP_X_INERTIA_PARTIAL_DATA")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def partial_except
|
|
39
|
+
split_header("HTTP_X_INERTIA_PARTIAL_EXCEPT")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset
|
|
43
|
+
split_header("HTTP_X_INERTIA_RESET")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def except_once
|
|
47
|
+
split_header("HTTP_X_INERTIA_EXCEPT_ONCE_PROPS")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def scroll_intent
|
|
51
|
+
@env["HTTP_X_INERTIA_INFINITE_SCROLL_MERGE_INTENT"]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Shaped to feed directly into ProtocolBuilder.new(partial: ...).
|
|
55
|
+
def partial_params
|
|
56
|
+
{
|
|
57
|
+
component: partial_component,
|
|
58
|
+
only: partial_only,
|
|
59
|
+
except: partial_except,
|
|
60
|
+
reset: reset,
|
|
61
|
+
except_once: except_once,
|
|
62
|
+
scroll_intent: scroll_intent
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def split_header(key)
|
|
69
|
+
value = @env[key]
|
|
70
|
+
return [] if value.nil? || value.empty?
|
|
71
|
+
|
|
72
|
+
value.split(",").map(&:strip).reject(&:empty?)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "digest"
|
|
6
|
+
require "json"
|
|
7
|
+
|
|
8
|
+
module InertiaHanami
|
|
9
|
+
# POSTs the Inertia page envelope to a separately-run Node SSR server and
|
|
10
|
+
# splices the returned markup into the layout in place of the CSR div
|
|
11
|
+
# (see Helper#inertia_ssr_head / #inertia_ssr_body).
|
|
12
|
+
#
|
|
13
|
+
# Responses are memoized in-process, keyed by a SHA256 digest of the page
|
|
14
|
+
# JSON, so repeat renders of an unchanged page skip the HTTP round-trip.
|
|
15
|
+
# The cache is a plain in-memory Hash - it is per-process and unbounded,
|
|
16
|
+
# not shared across app instances.
|
|
17
|
+
#
|
|
18
|
+
# On any failure (connection error, non-2xx response, malformed JSON),
|
|
19
|
+
# #call returns nil so the caller can fall back to CSR, unless
|
|
20
|
+
# `ssr.raise_on_error` is enabled, in which case the error propagates.
|
|
21
|
+
class SSRRenderer
|
|
22
|
+
Result = Struct.new(:head, :body)
|
|
23
|
+
|
|
24
|
+
def self.instance
|
|
25
|
+
@instance ||= new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(config: Hanami.app["inertia.config"])
|
|
29
|
+
@config = config
|
|
30
|
+
@cache = {}
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call(page)
|
|
35
|
+
json = page.to_json
|
|
36
|
+
digest = Digest::SHA256.hexdigest(json)
|
|
37
|
+
|
|
38
|
+
@mutex.synchronize { @cache[digest] } || fetch(json, digest)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def fetch(json, digest)
|
|
44
|
+
result = Result.new(*parse(post(json)).values_at("head", "body"))
|
|
45
|
+
result.head = Array(result.head).join("\n") if result.head.is_a?(Array)
|
|
46
|
+
@mutex.synchronize { @cache[digest] = result }
|
|
47
|
+
result
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
raise e if @config.ssr.raise_on_error
|
|
50
|
+
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def post(json)
|
|
55
|
+
uri = URI.join(@config.ssr.url, "/render")
|
|
56
|
+
response = Net::HTTP.post(uri, json, "Content-Type" => "application/json")
|
|
57
|
+
raise "SSR server responded with #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
58
|
+
|
|
59
|
+
response.body
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse(body)
|
|
63
|
+
JSON.parse(body)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|