@@ -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
@@ -97,15 +97,17 @@ def merge_props(shared_props, props, deep_merge)
9797 def computed_props
9898 # rubocop:disable Style/MultilineBlockChain
9999 @props
100- . tap do |merged_props | # Always keep errors in the props
101- if merged_props . key? ( : errors) && ! merged_props [ :errors ] . is_a? ( BaseProp )
102- errors = merged_props [ :errors ]
103- merged_props [ :errors ] = InertiaRails . always { errors }
104- end
100+ . tap do |merged_props |
101+ # Always keep errors in the props
102+ if merged_props . key? ( : errors) && ! merged_props [ :errors ] . is_a? ( BaseProp )
103+ errors = merged_props [ :errors ]
104+ merged_props [ :errors ] = InertiaRails . always { errors }
105105 end
106+ end
106107 . then { |props | deep_transform_props ( props ) } # Internal hydration/filtering
107108 . then { |props | @configuration . prop_transformer ( props : props ) } # Apply user-defined prop transformer
108- . tap do |props | # Add meta tags last (never transformed)
109+ . tap do |props |
110+ # Add meta tags last (never transformed)
109111 props [ :_inertia_meta ] = meta_tags if meta_tags . present?
110112 end
111113 # rubocop:enable Style/MultilineBlockChain
@@ -128,6 +130,9 @@ def page
128130 @page [ :scrollProps ] = scroll_props if scroll_props . present?
129131 @page . merge! ( resolve_merge_props )
130132
133+ once_props = resolve_once_props
134+ @page [ :onceProps ] = once_props if once_props . present?
135+
131136 @page
132137 end
133138
@@ -173,6 +178,17 @@ def resolve_merge_props
173178 } . delete_if { |_ , v | v . blank? }
174179 end
175180
181+ def resolve_once_props
182+ @props . each_with_object ( { } ) do |( key , prop ) , result |
183+ next unless prop . try ( :once? )
184+ next if excluded_by_partial_request? ( [ key . to_s ] )
185+
186+ once_key = ( prop . once_key || key ) . to_s
187+
188+ result [ once_key ] = { prop : key . to_s , expiresAt : prop . expires_at } . compact
189+ end
190+ end
191+
176192 def resolve_match_on_props
177193 all_merge_props . filter_map do |key , prop |
178194 prop . match_on . map! { |ms | "#{ key } .#{ ms } " } if prop . match_on . present?
@@ -251,6 +267,10 @@ def partial_except_keys
251267 @partial_except_keys ||= ( @request . headers [ 'X-Inertia-Partial-Except' ] || '' ) . split ( ',' ) . compact_blank!
252268 end
253269
270+ def except_once_keys
271+ @except_once_keys ||= ( @request . headers [ 'X-Inertia-Except-Once-Props' ] || '' ) . split ( ',' ) . compact_blank!
272+ end
273+
254274 def rendering_partial_component?
255275 @request . headers [ 'X-Inertia-Partial-Component' ] == @component
256276 end
@@ -265,19 +285,38 @@ def resolve_component(component)
265285
266286 def keep_prop? ( prop , path )
267287 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
288+ return false if excluded_by_once_cache? ( prop , path )
289+ return false if excluded_by_partial_request? ( path )
274290
275291 # Precedence: Evaluate IgnoreOnFirstLoadProp only after partial keys have been checked
276292 return false if prop . is_a? ( IgnoreOnFirstLoadProp ) && !rendering_partial_component?
277293
278294 true
279295 end
280296
297+ def excluded_by_once_cache? ( prop , path )
298+ return false unless prop . try ( :once? )
299+ return false if prop . try ( :fresh? )
300+ return false if explicitly_requested? ( path )
301+
302+ once_key = ( prop . once_key || path . join ( '.' ) ) . to_s
303+ except_once_keys . include? ( once_key )
304+ end
305+
306+ def explicitly_requested? ( path )
307+ return false unless rendering_partial_component? && partial_keys . present?
308+
309+ path_with_prefixes = path_prefixes ( path )
310+ ( path_with_prefixes & partial_keys ) . any?
311+ end
312+
313+ def excluded_by_partial_request? ( path )
314+ return false unless rendering_partial_component? && ( partial_keys . present? || partial_except_keys . present? )
315+
316+ path_with_prefixes = path_prefixes ( path )
317+ excluded_by_only_partial_keys? ( path_with_prefixes ) || excluded_by_except_partial_keys? ( path_with_prefixes )
318+ end
319+
281320 def path_prefixes ( parts )
282321 ( 0 ...parts . length ) . map do |i |
283322 parts [ 0 ..i ] . join ( '.' )
0 commit comments