aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@gmail.com>2011-04-16 10:28:47 +0200
committerJosé Valim <jose.valim@gmail.com>2011-04-16 10:28:47 +0200
commite30ca001efa861cc13259ca8287837174b24e679 (patch)
treeab47ef08d8c2e8773bb7fc6d6d24cda6cd32bf66 /actionpack
parent2dd43c3f804176d114cdbfeb8a0f92a43155baee (diff)
downloadrails-e30ca001efa861cc13259ca8287837174b24e679.tar.gz
rails-e30ca001efa861cc13259ca8287837174b24e679.tar.bz2
rails-e30ca001efa861cc13259ca8287837174b24e679.zip
Yo dawg, I heard you like streaming. So I put a fiber, inside a block, inside a body, so you can stream.
Diffstat (limited to 'actionpack')
-rw-r--r--actionpack/lib/action_view.rb12
-rw-r--r--actionpack/lib/action_view/base.rb47
-rw-r--r--actionpack/lib/action_view/buffers.rb43
-rw-r--r--actionpack/lib/action_view/flows.rb64
-rw-r--r--actionpack/lib/action_view/helpers/capture_helper.rb11
-rw-r--r--actionpack/lib/action_view/renderer/fibered_template_renderer.rb35
-rw-r--r--actionpack/lib/action_view/renderer/partial_renderer.rb2
-rw-r--r--actionpack/lib/action_view/renderer/streaming_template_renderer.rb79
-rw-r--r--actionpack/lib/action_view/renderer/template_renderer.rb1
-rw-r--r--actionpack/lib/action_view/rendering.rb26
-rw-r--r--actionpack/lib/action_view/template.rb6
-rw-r--r--actionpack/lib/action_view/template/handlers/erb.rb18
-rw-r--r--actionpack/test/template/capture_helper_test.rb2
13 files changed, 235 insertions, 111 deletions
diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb
index 9b8b694646..4547aceb28 100644
--- a/actionpack/lib/action_view.rb
+++ b/actionpack/lib/action_view.rb
@@ -44,7 +44,7 @@ module ActionView
autoload :AbstractRenderer
autoload :PartialRenderer
autoload :TemplateRenderer
- autoload :FiberedTemplateRenderer
+ autoload :StreamingTemplateRenderer
end
autoload_at "action_view/template/resolver" do
@@ -54,6 +54,16 @@ module ActionView
autoload :FallbackFileSystemResolver
end
+ autoload_at "action_view/buffers" do
+ autoload :OutputBuffer
+ autoload :StreamingBuffer
+ end
+
+ autoload_at "action_view/flows" do
+ autoload :OutputFlow
+ autoload :StreamingFlow
+ end
+
autoload_at "action_view/template/error" do
autoload :MissingTemplate
autoload :ActionViewError
diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb
index 10a523eeac..513080ae54 100644
--- a/actionpack/lib/action_view/base.rb
+++ b/actionpack/lib/action_view/base.rb
@@ -182,7 +182,7 @@ module ActionView #:nodoc:
@_config = {}
@_virtual_path = nil
- @_view_flow = Flow.new
+ @_view_flow = OutputFlow.new
@output_buffer = nil
if @_controller = controller
@@ -205,49 +205,4 @@ module ActionView #:nodoc:
ActiveSupport.run_load_hooks(:action_view, self)
end
-
- class Flow
- attr_reader :content
-
- def initialize
- @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new }
- end
-
- def get(key)
- @content[key]
- end
-
- def set(key, value)
- @content[key] = value
- end
-
- def append(key, value)
- @content[key] << value
- end
- end
-
- class FiberedFlow < Flow
- def initialize(flow, fiber)
- @content = flow.content
- @fiber = fiber
- end
-
- def get(key)
- return super if @content.key?(key)
-
- begin
- @waiting_for = key
- Fiber.yield
- ensure
- @waiting_for = nil
- end
-
- super
- end
-
- def set(key, value)
- super
- @fiber.resume if @waiting_for == key
- end
- end
end
diff --git a/actionpack/lib/action_view/buffers.rb b/actionpack/lib/action_view/buffers.rb
new file mode 100644
index 0000000000..2e2b39e4a2
--- /dev/null
+++ b/actionpack/lib/action_view/buffers.rb
@@ -0,0 +1,43 @@
+require 'active_support/core_ext/string/output_safety'
+
+module ActionView
+ class OutputBuffer < ActiveSupport::SafeBuffer
+ def initialize(*)
+ super
+ encode! if encoding_aware?
+ end
+
+ def <<(value)
+ super(value.to_s)
+ end
+ alias :append= :<<
+ alias :safe_append= :safe_concat
+ end
+
+ class StreamingBuffer
+ def initialize(block)
+ @block = block
+ end
+
+ def <<(value)
+ value = value.to_s
+ value = ERB::Util.h(value) unless value.html_safe?
+ @block.call(value)
+ end
+ alias :concat :<<
+ alias :append= :<<
+
+ def safe_concat(value)
+ @block.call(value.to_s)
+ end
+ alias :safe_append= :safe_concat
+
+ def html_safe?
+ true
+ end
+
+ def html_safe
+ self
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/flows.rb b/actionpack/lib/action_view/flows.rb
new file mode 100644
index 0000000000..1ac62961d1
--- /dev/null
+++ b/actionpack/lib/action_view/flows.rb
@@ -0,0 +1,64 @@
+require 'active_support/core_ext/string/output_safety'
+
+module ActionView
+ class OutputFlow
+ attr_reader :content
+
+ def initialize
+ @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new }
+ end
+
+ def get(key)
+ @content[key]
+ end
+
+ def set(key, value)
+ @content[key] = value
+ end
+
+ def append(key, value)
+ @content[key] << value
+ end
+ end
+
+ class StreamingFlow < OutputFlow
+ def initialize(flow, fiber)
+ @content = flow.content
+ @fiber = fiber
+ @root = Fiber.current.object_id
+ end
+
+ # Try to get an stored content. If the content
+ # is not available and we are inside the layout
+ # fiber, we set that we are waiting for the given
+ # key and yield.
+ def get(key)
+ return super if @content.key?(key)
+
+ if inside_fiber?
+ begin
+ @waiting_for = key
+ Fiber.yield
+ ensure
+ @waiting_for = nil
+ end
+ end
+
+ super
+ end
+
+ # Set the contents for the given key. This is called
+ # by provides and resumes back to the fiber if it is
+ # the key it is waiting for.
+ def set(key, value)
+ super
+ @fiber.resume if @waiting_for == key
+ end
+
+ private
+
+ def inside_fiber?
+ Fiber.current.object_id != @root
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb
index f8b5605ed9..148d814ac7 100644
--- a/actionpack/lib/action_view/helpers/capture_helper.rb
+++ b/actionpack/lib/action_view/helpers/capture_helper.rb
@@ -139,6 +139,17 @@ module ActionView
result unless content
end
+ # The same as +content_for+ but when used with streaming flushes
+ # straight back to the layout. In other words, if you want to
+ # concatenate several times to the same buffer when rendering a given
+ # template, you should use +content_for+, if not, use +provide+ as it
+ # has better streaming support.
+ def provide(name, content = nil, &block)
+ content = capture(&block) if block_given?
+ @_view_flow.set(name, content) if content
+ content
+ end
+
# content_for? simply checks whether any content has been captured yet using content_for
# Useful to render parts of your layout differently based on what is in your views.
#
diff --git a/actionpack/lib/action_view/renderer/fibered_template_renderer.rb b/actionpack/lib/action_view/renderer/fibered_template_renderer.rb
deleted file mode 100644
index 45f48cab76..0000000000
--- a/actionpack/lib/action_view/renderer/fibered_template_renderer.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'action_view/renderer/template_renderer'
-require 'fiber'
-
-module ActionView
- class FiberedTemplateRenderer < TemplateRenderer #:nodoc:
- # Renders the given template. An string representing the layout can be
- # supplied as well.
- def render_template(template, layout_name = nil, locals = {}) #:nodoc:
- view, locals = @view, locals || {}
-
- final = nil
- layout = layout_name && find_layout(layout_name, locals.keys)
- yielder = lambda { |*name| view._layout_for(*name) }
-
- instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
- @fiber = Fiber.new do
- final = if layout
- layout.render(view, locals, &yielder)
- else
- view._layout_for
- end
- end
-
- @view._view_flow = FiberedFlow.new(view._view_flow, @fiber)
- @fiber.resume
-
- content = template.render(view, locals, &yielder)
- view._view_flow.set(:layout, content)
- @fiber.resume while @fiber.alive?
- end
-
- final
- end
- end
-end
diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb
index 180afc27ac..10cd37d56f 100644
--- a/actionpack/lib/action_view/renderer/partial_renderer.rb
+++ b/actionpack/lib/action_view/renderer/partial_renderer.rb
@@ -1,5 +1,3 @@
-require 'action_view/renderer/abstract_renderer'
-
module ActionView
class PartialRenderer < AbstractRenderer #:nodoc:
PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} }
diff --git a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb
new file mode 100644
index 0000000000..a8c6ecbde1
--- /dev/null
+++ b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb
@@ -0,0 +1,79 @@
+require 'fiber'
+
+module ActionView
+ class StreamingTemplateRenderer < TemplateRenderer #:nodoc:
+ # A valid Rack::Body (i.e. it responds to each).
+ # It is initialized with a block that, when called, starts
+ # rendering the template.
+ class Body #:nodoc:
+ def initialize(&start)
+ @start = start
+ end
+
+ def each(&block)
+ @start.call(block)
+ self
+ end
+ end
+
+ # For streaming, instead of rendering a given a template, we return a Body
+ # object that responds to each. This object is initialized with a block
+ # that knows how to render the template.
+ def render_template(template, layout_name = nil, locals = {}) #:nodoc:
+ return [super] unless template.supports_streaming?
+
+ locals ||= {}
+ layout = layout_name && find_layout(layout_name, locals.keys)
+
+ Body.new do |buffer|
+ delayed_render(buffer, template, layout, @view, locals)
+ end
+ end
+
+ private
+
+ def delayed_render(buffer, template, layout, view, locals)
+ # Wrap the given buffer in the StreamingBuffer and pass it to the
+ # underlying template handler. Now, everytime something is concatenated
+ # to the buffer, it is not appended to an array, but streamed straight
+ # to the client.
+ output = ActionView::StreamingBuffer.new(buffer)
+ yielder = lambda { |*name| view._layout_for(*name) }
+
+ instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
+ fiber = Fiber.new do
+ if layout
+ layout.render(view, locals, output, &yielder)
+ else
+ # If you don't have a layout, just render the thing
+ # and concatenate the final result. This is the same
+ # as a layout with just <%= yield %>
+ output.safe_concat view._layout_for
+ end
+ end
+
+ # Set the view flow to support streaming. It will be aware
+ # when to stop rendering the layout because it needs to search
+ # something in the template and vice-versa.
+ view._view_flow = StreamingFlow.new(view._view_flow, fiber)
+
+ # Yo! Start the fiber!
+ fiber.resume
+
+ # If the fiber is still alive, it means we need something
+ # from the template, so start rendering it. If not, it means
+ # the layout exited without requiring anything from the template.
+ if fiber.alive?
+ content = template.render(view, locals, &yielder)
+
+ # Once rendering the template is done, sets its content in the :layout key.
+ view._view_flow.set(:layout, content)
+
+ # In case the layout continues yielding, we need to resume
+ # the fiber until all yields are handled.
+ fiber.resume while fiber.alive?
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb
index 10d8cc3b87..6b5ead463f 100644
--- a/actionpack/lib/action_view/renderer/template_renderer.rb
+++ b/actionpack/lib/action_view/renderer/template_renderer.rb
@@ -1,6 +1,5 @@
require 'active_support/core_ext/object/try'
require 'active_support/core_ext/array/wrap'
-require 'action_view/renderer/abstract_renderer'
module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
diff --git a/actionpack/lib/action_view/rendering.rb b/actionpack/lib/action_view/rendering.rb
index e3568e70e5..2bce2fb045 100644
--- a/actionpack/lib/action_view/rendering.rb
+++ b/actionpack/lib/action_view/rendering.rb
@@ -27,6 +27,19 @@ module ActionView
end
end
+ # Render but returns a valid Rack body. If fibers are defined, we return
+ # a streaming body that renders the template piece by piece.
+ #
+ # Note that partials are not supported to be rendered with streaming,
+ # so in such cases, we just wrap them in an array.
+ def render_body(options)
+ if options.key?(:partial)
+ [_render_partial(options)]
+ else
+ StreamingTemplateRenderer.new(self).render(options)
+ end
+ end
+
# Returns the contents that are yielded to a layout, given a name or a block.
#
# You can think of a layout as a method that is called with a block. If the user calls
@@ -79,7 +92,7 @@ module ActionView
@_view_flow.get(name).html_safe
end
- # Returns the content from the Flow unless we have a block.
+ # Handle layout for calls from partials that supports blocks.
def _block_layout_for(*args, &block)
name = args.first
@@ -91,20 +104,11 @@ module ActionView
end
def _render_template(options) #:nodoc:
- if @magic_medicine
- _fibered_template_renderer.render(options)
- else
- _template_renderer.render(options)
- end
+ _template_renderer.render(options)
end
def _template_renderer #:nodoc:
@_template_renderer ||= TemplateRenderer.new(self)
end
-
- def _fibered_template_renderer #:nodoc:
- @_fibered_template_renderer ||= FiberedTemplateRenderer.new(self)
- end
-
end
end
diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb
index 17e549a1c2..6dfc4f68ae 100644
--- a/actionpack/lib/action_view/template.rb
+++ b/actionpack/lib/action_view/template.rb
@@ -126,6 +126,12 @@ module ActionView
@formats = Array.wrap(format).map { |f| f.is_a?(Mime::Type) ? f.ref : f }
end
+ # Returns if the underlying handler supports streaming. If so,
+ # a streaming buffer *may* be passed when it start rendering.
+ def supports_streaming?
+ handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
+ end
+
# Render a template. If the template was not compiled yet, it is done
# exactly before rendering.
#
diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb
index 4af576a688..7e9e4e518a 100644
--- a/actionpack/lib/action_view/template/handlers/erb.rb
+++ b/actionpack/lib/action_view/template/handlers/erb.rb
@@ -1,23 +1,9 @@
require 'active_support/core_ext/class/attribute_accessors'
-require 'active_support/core_ext/string/output_safety'
require 'action_view/template'
require 'action_view/template/handler'
require 'erubis'
module ActionView
- class OutputBuffer < ActiveSupport::SafeBuffer
- def initialize(*)
- super
- encode! if encoding_aware?
- end
-
- def <<(value)
- super(value.to_s)
- end
- alias :append= :<<
- alias :safe_append= :safe_concat
- end
-
class Template
module Handlers
class Erubis < ::Erubis::Eruby
@@ -73,6 +59,10 @@ module ActionView
new.call(template)
end
+ def supports_streaming?
+ true
+ end
+
def handles_encoding?
true
end
diff --git a/actionpack/test/template/capture_helper_test.rb b/actionpack/test/template/capture_helper_test.rb
index ec252fa117..3ebcb8165f 100644
--- a/actionpack/test/template/capture_helper_test.rb
+++ b/actionpack/test/template/capture_helper_test.rb
@@ -4,7 +4,7 @@ class CaptureHelperTest < ActionView::TestCase
def setup
super
@av = ActionView::Base.new
- @_view_flow = ActionView::Flow.new
+ @_view_flow = ActionView::OutputFlow.new
end
def test_capture_captures_the_temporary_output_buffer_in_its_block