aboutsummaryrefslogtreecommitdiffstats
path: root/actionview/lib/action_view
diff options
context:
space:
mode:
Diffstat (limited to 'actionview/lib/action_view')
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb31
-rw-r--r--actionview/lib/action_view/railtie.rb6
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb8
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb70
-rw-r--r--actionview/lib/action_view/template.rb14
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb18
6 files changed, 145 insertions, 2 deletions
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index 9dadfb5ce1..0e2a5f90f4 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -110,6 +110,29 @@ module ActionView
# <%= some_helper_method(person) %>
#
# Now all you'll have to do is change that timestamp when the helper method changes.
+ #
+ # === Automatic Collection Caching
+ #
+ # When rendering collections such as:
+ #
+ # <%= render @notifications %>
+ # <%= render partial: 'notifications/notification', collection: @notifications %>
+ #
+ # If the notifications/_notification partial starts with a cache call like so:
+ #
+ # <% cache notification do %>
+ # <%= notification.name %>
+ # <% end %>
+ #
+ # The collection can then automatically use any cached renders for that
+ # template by reading them at once instead of one by one.
+ #
+ # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for more
+ # information on what cache calls make a template eligible for this collection caching.
+ #
+ # The automatic cache multi read can be turned off like so:
+ #
+ # <%= render @notifications, cache: false %>
def cache(name = {}, options = nil, &block)
if controller.perform_caching
safe_concat(fragment_for(cache_fragment_name(name, options), options, &block))
@@ -161,6 +184,14 @@ module ActionView
end
end
+ # Given a key (as described in ActionController::Caching::Fragments.expire_fragment),
+ # returns a key suitable for use in reading, writing, or expiring a
+ # cached fragment. All keys are prefixed with <tt>views/</tt> and uses
+ # ActiveSupport::Cache.expand_cache_key for the expansion.
+ def fragment_cache_key(key)
+ ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
+ end
+
private
def fragment_name_with_digest(name) #:nodoc:
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index ee3dce24b7..9a26cba574 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -36,6 +36,12 @@ module ActionView
end
end
+ initializer "action_view.collection_caching" do |app|
+ ActiveSupport.on_load(:action_controller) do
+ PartialRenderer.collection_cache = app.config.action_controller.cache_store
+ end
+ end
+
initializer "action_view.setup_action_pack" do |app|
ActiveSupport.on_load(:action_controller) do
ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 5ff15411cf..56b8ab1e2d 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -1,3 +1,4 @@
+require 'action_view/renderer/partial_renderer/collection_caching'
require 'thread_safe'
module ActionView
@@ -280,6 +281,8 @@ module ActionView
# <%- end -%>
# <% end %>
class PartialRenderer < AbstractRenderer
+ include CollectionCaching
+
PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k|
h[k] = ThreadSafe::Cache.new
end
@@ -321,8 +324,9 @@ module ActionView
spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
end
- result = @template ? collection_with_template : collection_without_template
- result.join(spacer).html_safe
+ cache_collection_render do
+ @template ? collection_with_template : collection_without_template
+ end.join(spacer).html_safe
end
def render_partial
diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
new file mode 100644
index 0000000000..b77c884e66
--- /dev/null
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -0,0 +1,70 @@
+require 'active_support/core_ext/object/try'
+
+module ActionView
+ module CollectionCaching # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ # Fallback cache store if Action View is used without Rails.
+ # Otherwise overriden in Railtie to use Rails.cache.
+ mattr_accessor(:collection_cache) { ActiveSupport::Cache::MemoryStore.new }
+ end
+
+ private
+ def cache_collection_render
+ return yield unless cache_collection?
+
+ keyed_collection = collection_by_cache_keys
+ partial_cache = collection_cache.read_multi(*keyed_collection.keys)
+
+ @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values
+ rendered_partials = @collection.any? ? yield.dup : []
+
+ fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do
+ rendered_partials.shift
+ end
+ end
+
+ def cache_collection?
+ @options.fetch(:cache, automatic_cache_eligible?)
+ end
+
+ def automatic_cache_eligible?
+ single_template_render? && !callable_cache_key? &&
+ @template.eligible_for_collection_caching?(as: @options[:as])
+ end
+
+ def single_template_render?
+ @template # Template is only set when a collection renders one template.
+ end
+
+ def callable_cache_key?
+ @options[:cache].respond_to?(:call)
+ end
+
+ def collection_by_cache_keys
+ seed = callable_cache_key? ? @options[:cache] : ->(i) { i }
+
+ @collection.each_with_object({}) do |item, hash|
+ hash[expanded_cache_key(seed.call(item))] = item
+ end
+ end
+
+ def expanded_cache_key(key)
+ key = @view.fragment_cache_key(@view.cache_fragment_name(key))
+ key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
+ end
+
+ def fetch_or_cache_partial(cached_partials, order_by:)
+ cache_options = @options[:cache_options] || @locals[:cache_options] || {}
+
+ order_by.map do |key|
+ cached_partials.fetch(key) do
+ yield.tap do |rendered_partial|
+ collection_cache.write(key, rendered_partial, cache_options)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 305d9c6ee3..377ceb534a 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -130,6 +130,7 @@ module ActionView
@source = source
@identifier = identifier
@handler = handler
+ @cache_name = extract_resource_cache_call_name
@compiled = false
@original_encoding = nil
@locals = details[:locals] || []
@@ -165,6 +166,10 @@ module ActionView
@type ||= Types[@formats.first] if @formats.first
end
+ def eligible_for_collection_caching?(as: nil)
+ @cache_name == (as || inferred_cache_name).to_s
+ end
+
# Receives a view object and return a template similar to self by using @virtual_path.
#
# This method is useful if you have a template object but it does not contain its source
@@ -345,5 +350,14 @@ module ActionView
payload = { virtual_path: @virtual_path, identifier: @identifier }
ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block)
end
+
+ def extract_resource_cache_call_name
+ $1 if @handler.respond_to?(:resource_cache_call_pattern) &&
+ @source =~ @handler.resource_cache_call_pattern
+ end
+
+ def inferred_cache_name
+ @inferred_cache_name ||= @virtual_path.split('/').last.sub('_', '')
+ end
end
end
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index 85a100ed4c..88a8570706 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -123,6 +123,24 @@ module ActionView
).src
end
+ # Returns Regexp to extract a cached resource's name from a cache call at the
+ # first line of a template.
+ # The extracted cache name is expected in $1.
+ #
+ # <% cache notification do %> # => notification
+ #
+ # The pattern should support templates with a beginning comment:
+ #
+ # <%# Still extractable even though there's a comment %>
+ # <% cache notification do %> # => notification
+ #
+ # But fail to extract a name if a resource association is cached.
+ #
+ # <% cache notification.event do %> # => nil
+ def resource_cache_call_pattern
+ /\A(?:<%#.*%>\n?)?<% cache\(?\s*(\w+\.?)/
+ end
+
private
def valid_encoding(string, encoding)