Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 977b8df503
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 330 lines (291 sloc) 9.952 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
module TemplateStreaming
  class << self
    def configure(config)
      config.each do |key, value|
        send "#{key}=", value
      end
    end

    #
    # If true, always reference the flash before returning from the
    # action when rendering progressively.
    #
    # This is required for the flash to work with progressive
    # rendering, but unlike standard Rails behavior, will cause the
    # flash to be swept even if it's never referenced in the
    # views. This usually isn't an issue, as flash messages are
    # typically rendered in the layout, causing a reference anyway.
    #
    # Default: true.
    #
    attr_accessor :autosweep_flash

    #
    # If true, always set the authenticity token before returning from
    # the action when rendering progressively.
    #
    # This is required for the authenticity token to work with
    # progressive rendering, but unlike standard Rails behavior, will
    # cause the token to be set (and thus the session updated) even if
    # it's never referenced in views.
    #
    # Default: true.
    #
    attr_accessor :set_authenticity_token
  end

  self.autosweep_flash = true
  self.set_authenticity_token = true

  module Controller
    def self.included(base)
      base.class_eval do
        alias_method_chain :render, :template_streaming
        alias_method_chain :render_to_string, :template_streaming
        helper_method :flush, :push

        include ActiveSupport::Callbacks
        define_callbacks :when_streaming_template
      end
    end

    def render_with_template_streaming(*args, &block)
      push_render_stack_frame do |stack_height|
        if start_rendering_progressively?(stack_height, *args)
          @render_progressively = true
          @template.render_progressively = true
          @performed_render = true
          @streaming_body = StreamingBody.new(progressive_rendering_threshold) do
            @performed_render = false
            last_piece = render_without_template_streaming(*args, &block)
            # The original render will clobber our response.body, so
            # we must push the buffer ourselves.
            push last_piece
          end
          response.body = @streaming_body
          response.prepare!
          flash if TemplateStreaming.autosweep_flash
          form_authenticity_token if TemplateStreaming.set_authenticity_token
          run_callbacks :when_streaming_template

          # Normally, @_flash is removed after #perform_action, which
          # means calling #flash in the view would cause a new
          # FlashHash to be constructed. On top of that, the flash is
          # swept on construction, which results in sweeping the flash
          # twice, obliterating its contents.
          #
          # So, we preserve the flash here under a different ivar, and
          # override the #flash helper to return it.
          if defined?(@_flash)
            @template_streaming_flash = @_flash
          end
        else
          render_without_template_streaming(*args, &block)
        end
      end
    end

    # Override to ensure calling render_to_string from a helper
    # doesn't trigger template streaming.
    def render_to_string_with_template_streaming(*args, &block) # :nodoc
      push_render_stack_frame do
        render_to_string_without_template_streaming(*args, &block)
      end
    end

    #
    # Flush the current template's output buffer out to the client
    # immediately.
    #
    def flush
      if @streaming_body && !@template.output_buffer.nil?
        push @template.output_buffer.slice!(0..-1)
      end
    end

    #
    # Push the given data to the client immediately.
    #
    def push(data)
      if @streaming_body
        @streaming_body.push(data)
        flush_thin
      end
    end

    def template_streaming_flash # :nodoc:
      @template_streaming_flash
    end

    def render_progressively?
      @render_progressively
    end

    private # --------------------------------------------------------

    def push_render_stack_frame
      @render_stack_height ||= 0
      @render_stack_height += 1
      begin
        yield @render_stack_height
      ensure
        @render_stack_height -= 1
      end
    end

    def start_rendering_progressively?(render_stack_height, *render_args)
      render_stack_height == 1 or
        return false

      (render_options = render_args.last).is_a?(Hash) or
        render_options = {}

      if !(UNSTREAMABLE_KEYS & render_options.keys).empty? || render_args.first == :update
        false
      else
        render_options[:progressive]
      end
    end

    UNSTREAMABLE_KEYS = [:text, :xml, :json, :js, :update, :nothing]

    #
    # The number of bytes that must be received by the client before
    # anything will be rendered.
    #
    def progressive_rendering_threshold
      content_type = response.header['Content-type']
      content_type.nil? || content_type =~ %r'\Atext/html' or
        return 0

      case request.env['HTTP_USER_AGENT']
      when /MSIE/
        255
      when /Chrome/
        # Note: Chrome's UA string includes "Safari", so it must precede.
        2048
      when /Safari/
        1024
      else
        0
      end
    end

    #
    # Force EventMachine to flush its buffer when using Thin.
    #
    def flush_thin
      connection = request.env['template_streaming.thin_connection'] and
        EventMachineFlush.flush(connection)
    end
  end

  # Only prepare once.
  module Response
    def self.included(base)
      base.alias_method_chain :prepare!, :template_streaming
      base.alias_method_chain :set_content_length!, :template_streaming
    end

    def prepare_with_template_streaming!
      return if defined?(@prepared)
      prepare_without_template_streaming!
      @prepared = true
    end

    def set_content_length_with_template_streaming!
      if body.is_a?(StreamingBody)
        # pass
      else
        set_content_length_without_template_streaming!
      end
    end
  end

  module View
    def self.included(base)
      base.alias_method_chain :_render_with_layout, :template_streaming
      base.alias_method_chain :flash, :template_streaming
    end

    attr_writer :render_progressively

    def render_progressively?
      @render_progressively
    end

    def _render_with_layout_with_template_streaming(options, local_assigns, &block)
      if !render_progressively? || block_given?
        _render_with_layout_without_template_streaming(options, local_assigns, &block)
      elsif options[:layout].is_a?(ActionView::Template)
        # Toplevel render call, from the controller.
        layout = options.delete(:layout)
        with_render_proc_for_layout(options) do
          render(options.merge(:file => layout.path_without_format_and_extension))
        end
      else
        layout = options.delete(:layout)
        with_render_proc_for_layout(options) do
          if (options[:inline] || options[:file] || options[:text])
            render(:file => layout, :locals => local_assigns)
          else
            render(options.merge(:partial => layout))
          end
        end
      end
    end

    def with_render_proc_for_layout(options)
      original_proc_for_layout = @_proc_for_layout
      @_proc_for_layout = lambda do |*args|
        if args.empty?
          render(options)
        else
          instance_variable_get(:"@content_for_#{args.first}")
        end
      end
      begin
        # TODO: what is @cached_content_for_layout in base.rb ?
        yield
      ensure
        @_proc_for_layout = original_proc_for_layout
      end
    end

    def flash_with_template_streaming # :nodoc:
      if render_progressively?
        # Flash has been swept - don't use the standard #flash or it'll sweep again.
        controller.instance_eval { @template_streaming_flash }
      else
        flash_without_template_streaming
      end
    end
  end

  class StreamingBody
    def initialize(threshold, &block)
      @process = block
      @bytes_to_threshold = threshold
    end

    def each(&block)
      @push = block
      @process.call
    end

    def push(data)
      if @bytes_to_threshold > 0
        @push.call(data + padding(@bytes_to_threshold - data.length))
        @bytes_to_threshold = 0
      else
        @push.call(data)
      end
    end

    private # -------------------------------------------------------

    def padding(length)
      return '' if length <= 0
      content_length = [length - 7, 0].max
      "<!--#{'+'*content_length}-->"
    end
  end

  ActionView::Base.send :include, View
  ActionController::Base.send :include, Controller
  ActionController::Response.send :include, Response
  ActionController::Dispatcher.middleware.insert 0, Rack::Chunked
end

# Please let there be a better way to do this...
#
# We need to force Thin (EventMachine, really) to flush its output
# buffer before ending the current EventMachine tick. We can't use
# EventMachine.defer or .next_tick, as that would require returning
# from the call to the response body's #each. I'm not convinced Thin
# could even be rearchitected to support this without resorting to
# Threads, Continuations, or Fibers.
#
# Here, we hack Thin to add a handle to the connection object to the
# request environment, which we pass to EventMachineFlush, a horrid
# C++ hack. In ruby 1.8.7 we could use env[async.callback].receiver,
# but we want to support 1.8.6 for now too.
if defined?(Thin)
  begin
    require 'event_machine_flush'
  rescue LoadError
    raise "Template Streaming on Thin requires the event_machine_flush gem."
  end

  Rails.configuration.after_initialize do
    Thin::Connection.class_eval do
      def pre_process_with_template_streaming(*args, &block)
        @request.env['template_streaming.thin_connection'] = self
        pre_process_without_template_streaming(*args, &block)
      end
      alias_method_chain :pre_process, :template_streaming
    end
  end
end
Something went wrong with that request. Please try again.