Skip to content

Commit 15fa9d5

Browse files
committed
Support InertiaRails.once prop
1 parent 3d7695c commit 15fa9d5

File tree

13 files changed

+631
-10
lines changed

13 files changed

+631
-10
lines changed

lib/inertia_rails/base_prop.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
module InertiaRails
44
# Base class for all props.
55
class BaseProp
6-
def initialize(&block)
6+
def initialize(**, &block)
77
@block = block
88
end
99

lib/inertia_rails/defer_prop.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module InertiaRails
44
class DeferProp < IgnoreOnFirstLoadProp
5+
prepend PropOnceable
56
prepend PropMergeable
67

78
DEFAULT_GROUP = 'default'

lib/inertia_rails/inertia_rails.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require 'inertia_rails/prop_onceable'
34
require 'inertia_rails/prop_mergeable'
45
require 'inertia_rails/base_prop'
56
require 'inertia_rails/ignore_on_first_load_prop'
@@ -8,6 +9,7 @@
89
require 'inertia_rails/optional_prop'
910
require 'inertia_rails/defer_prop'
1011
require 'inertia_rails/merge_prop'
12+
require 'inertia_rails/once_prop'
1113
require 'inertia_rails/scroll_prop'
1214
require 'inertia_rails/configuration'
1315
require 'inertia_rails/meta_tag'
@@ -36,6 +38,10 @@ def always(&block)
3638
AlwaysProp.new(&block)
3739
end
3840

41+
def once(...)
42+
OnceProp.new(...)
43+
end
44+
3945
def merge(...)
4046
MergeProp.new(...)
4147
end
@@ -44,8 +50,8 @@ def deep_merge(match_on: nil, &block)
4450
MergeProp.new(deep_merge: true, match_on: match_on, &block)
4551
end
4652

47-
def defer(group: nil, merge: nil, deep_merge: nil, match_on: nil, &block)
48-
DeferProp.new(group: group, merge: merge, deep_merge: deep_merge, match_on: match_on, &block)
53+
def defer(...)
54+
DeferProp.new(...)
4955
end
5056

5157
def scroll(metadata = nil, **options, &block)

lib/inertia_rails/merge_prop.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
module InertiaRails
44
class MergeProp < BaseProp
5+
prepend PropOnceable
56
prepend PropMergeable
67

78
def initialize(**_props, &block)

lib/inertia_rails/once_prop.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module InertiaRails
4+
class OnceProp < BaseProp
5+
prepend PropOnceable
6+
7+
def initialize(**, &block)
8+
@once = true
9+
super(&block)
10+
end
11+
end
12+
end

lib/inertia_rails/optional_prop.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
module InertiaRails
44
class OptionalProp < IgnoreOnFirstLoadProp
5+
prepend PropOnceable
56
end
67
end

lib/inertia_rails/prop_onceable.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
module InertiaRails
4+
module PropOnceable
5+
attr_reader :once_key, :once_ttl
6+
7+
def initialize(**props, &block)
8+
@once = props.fetch(:once, false)
9+
@once_key = props[:key]
10+
@once_ttl = props[:ttl]
11+
@fresh = props.fetch(:fresh, false)
12+
13+
super
14+
end
15+
16+
def once?
17+
@once
18+
end
19+
20+
def fresh?
21+
@fresh
22+
end
23+
24+
def expires_at
25+
return nil unless @once_ttl
26+
27+
((Time.current.to_f + @once_ttl) * 1000).to_i
28+
end
29+
end
30+
end

lib/inertia_rails/renderer.rb

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def render
4848
else
4949
"#{@response.headers['Vary']}, X-Inertia"
5050
end
51-
if @request.headers['X-Inertia']
51+
if @request.inertia?
5252
@response.set_header('X-Inertia', 'true')
5353
@render_method.call json: page.to_json, status: @response.status, content_type: Mime[:json]
5454
else
@@ -128,6 +128,9 @@ def page
128128
@page[:scrollProps] = scroll_props if scroll_props.present?
129129
@page.merge!(resolve_merge_props)
130130

131+
once_props = resolve_once_props
132+
@page[:onceProps] = once_props if once_props.present?
133+
131134
@page
132135
end
133136

@@ -173,6 +176,18 @@ def resolve_merge_props
173176
}.delete_if { |_, v| v.blank? }
174177
end
175178

179+
def resolve_once_props
180+
@props.each_with_object({}) do |(key, prop), result|
181+
next unless prop.try(:once?)
182+
next if excluded_by_once_cache?(prop, [key.to_s])
183+
next if excluded_by_partial_request?([key.to_s])
184+
185+
once_key = (prop.once_key || key).to_s
186+
187+
result[once_key] = { prop: key.to_s, expiresAt: prop.expires_at }.compact
188+
end
189+
end
190+
176191
def resolve_match_on_props
177192
all_merge_props.filter_map do |key, prop|
178193
prop.match_on.map! { |ms| "#{key}.#{ms}" } if prop.match_on.present?
@@ -251,6 +266,10 @@ def partial_except_keys
251266
@partial_except_keys ||= (@request.headers['X-Inertia-Partial-Except'] || '').split(',').compact_blank!
252267
end
253268

269+
def except_once_keys
270+
@except_once_keys ||= (@request.headers['X-Inertia-Except-Once-Props'] || '').split(',').compact_blank!
271+
end
272+
254273
def rendering_partial_component?
255274
@request.headers['X-Inertia-Partial-Component'] == @component
256275
end
@@ -265,19 +284,30 @@ def resolve_component(component)
265284

266285
def keep_prop?(prop, path)
267286
return true if prop.is_a?(AlwaysProp)
268-
269-
if rendering_partial_component? && (partial_keys.present? || partial_except_keys.present?)
270-
path_with_prefixes = path_prefixes(path)
271-
return false if excluded_by_only_partial_keys?(path_with_prefixes)
272-
return false if excluded_by_except_partial_keys?(path_with_prefixes)
273-
end
287+
return false if excluded_by_once_cache?(prop, path)
288+
return false if excluded_by_partial_request?(path)
274289

275290
# Precedence: Evaluate IgnoreOnFirstLoadProp only after partial keys have been checked
276291
return false if prop.is_a?(IgnoreOnFirstLoadProp) && !rendering_partial_component?
277292

278293
true
279294
end
280295

296+
def excluded_by_once_cache?(prop, path)
297+
return false unless prop.try(:once?)
298+
return false if prop.try(:fresh?)
299+
300+
once_key = (prop.once_key || path.join('.')).to_s
301+
except_once_keys.include?(once_key)
302+
end
303+
304+
def excluded_by_partial_request?(path)
305+
return false unless rendering_partial_component? && (partial_keys.present? || partial_except_keys.present?)
306+
307+
path_with_prefixes = path_prefixes(path)
308+
excluded_by_only_partial_keys?(path_with_prefixes) || excluded_by_except_partial_keys?(path_with_prefixes)
309+
end
310+
281311
def path_prefixes(parts)
282312
(0...parts.length).map do |i|
283313
parts[0..i].join('.')

spec/dummy/app/controllers/inertia_render_test_controller.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,73 @@ def reset_test
189189
regular_prop: 'regular value',
190190
}
191191
end
192+
193+
def once_props
194+
render inertia: 'TestComponent', props: {
195+
cached_data: InertiaRails.once { 'expensive data' },
196+
regular: 'regular prop',
197+
}
198+
end
199+
200+
def once_props_with_ttl
201+
render inertia: 'TestComponent', props: {
202+
cached_data: InertiaRails.once(ttl: 3600) { 'expensive data with ttl' },
203+
}
204+
end
205+
206+
def once_props_with_custom_key
207+
render inertia: 'TestComponent', props: {
208+
cached_data: InertiaRails.once(key: 'my_custom_key') { 'expensive data with custom key' },
209+
}
210+
end
211+
212+
def deferred_once_props
213+
render inertia: 'TestComponent', props: {
214+
name: 'Brian',
215+
deferred_cached: InertiaRails.defer(once: true) { 'deferred and cached' },
216+
}
217+
end
218+
219+
inertia_share only: [:shared_once_props] do
220+
{
221+
shared_cached: InertiaRails.once { 'shared once data' },
222+
}
223+
end
224+
225+
def shared_once_props
226+
render inertia: 'TestComponent', props: {
227+
name: 'Brian',
228+
}
229+
end
230+
231+
def nested_once_props
232+
render inertia: 'TestComponent', props: {
233+
nested: {
234+
cached: InertiaRails.once { 'nested cached data' },
235+
},
236+
regular: 'regular prop',
237+
}
238+
end
239+
240+
def multiple_once_props
241+
render inertia: 'TestComponent', props: {
242+
cached_one: InertiaRails.once { 'first cached' },
243+
cached_two: InertiaRails.once { 'second cached' },
244+
regular: 'regular prop',
245+
}
246+
end
247+
248+
def once_props_not_fresh
249+
render inertia: 'TestComponent', props: {
250+
cached_data: InertiaRails::OnceProp.new { 'cached data' },
251+
regular: 'regular prop',
252+
}
253+
end
254+
255+
def once_props_fresh
256+
render inertia: 'TestComponent', props: {
257+
cached_data: InertiaRails::OnceProp.new(fresh: true) { 'fresh data' },
258+
regular: 'regular prop',
259+
}
260+
end
192261
end

spec/dummy/config/routes.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@
4444
get 'prepend_merge_test' => 'inertia_render_test#prepend_merge_test'
4545
get 'nested_paths_test' => 'inertia_render_test#nested_paths_test'
4646
get 'reset_test' => 'inertia_render_test#reset_test'
47+
get 'once_props' => 'inertia_render_test#once_props'
48+
get 'once_props_with_ttl' => 'inertia_render_test#once_props_with_ttl'
49+
get 'once_props_with_custom_key' => 'inertia_render_test#once_props_with_custom_key'
50+
get 'deferred_once_props' => 'inertia_render_test#deferred_once_props'
51+
get 'shared_once_props' => 'inertia_render_test#shared_once_props'
52+
get 'nested_once_props' => 'inertia_render_test#nested_once_props'
53+
get 'multiple_once_props' => 'inertia_render_test#multiple_once_props'
54+
get 'once_props_not_fresh' => 'inertia_render_test#once_props_not_fresh'
55+
get 'once_props_fresh' => 'inertia_render_test#once_props_fresh'
4756
get 'non_inertiafied' => 'inertia_test#non_inertiafied'
4857
get 'deeply_nested_props' => 'inertia_render_test#deeply_nested_props'
4958

0 commit comments

Comments
 (0)