template_streaming 0.0.11 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +9 -0
- data/README.markdown +177 -88
- data/Rakefile +0 -21
- data/lib/template_streaming.rb +184 -99
- data/lib/template_streaming/autoflushing.rb +88 -0
- data/lib/template_streaming/caching.rb +68 -0
- data/lib/template_streaming/error_recovery.rb +199 -85
- data/lib/template_streaming/new_relic.rb +555 -0
- data/lib/template_streaming/templates/errors.erb +37 -0
- data/lib/template_streaming/version.rb +1 -1
- data/rails/init.rb +3 -0
- data/spec/autoflushing_spec.rb +75 -0
- data/spec/caching_spec.rb +126 -0
- data/spec/error_recovery_spec.rb +261 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/streaming_app.rb +135 -0
- data/spec/template_streaming_spec.rb +926 -0
- metadata +55 -27
data/CHANGELOG
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
== 0.1.0 2011-05-16
|
2
|
+
|
3
|
+
* Replace prelayouts with :stream => true option to #render.
|
4
|
+
* Controller-level #stream to stream selected actions.
|
5
|
+
* Inject errors during rendering to end of body.
|
6
|
+
* Autoflushing.
|
7
|
+
* Fix page and action caching for streamed responses.
|
8
|
+
* New Relic support.
|
9
|
+
|
1
10
|
== 0.0.11 2010-05-10
|
2
11
|
|
3
12
|
* Ensure authenticity token is initialized properly.
|
data/README.markdown
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Template Streaming
|
2
2
|
|
3
|
-
|
3
|
+
Progressive rendering for Rails.
|
4
4
|
|
5
5
|
## Background
|
6
6
|
|
@@ -8,33 +8,45 @@ A typical Rails client-side profile looks something like this:
|
|
8
8
|
|
9
9
|
![Typical Rails Profile][slow-profile]
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
This is highly suboptimal. Many resources, such as external stylesheets, are
|
12
|
+
completely static and could be loaded by the client while it's waiting for the
|
13
|
+
server response.
|
14
14
|
|
15
|
-
The trick is to
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
page to become interactive.
|
15
|
+
The trick is to *stream* the response--flushing the markup for the static
|
16
|
+
resources to the client before it has rendered the rest of the page. In
|
17
|
+
addition to being able to render styles and images earlier, the browser can
|
18
|
+
download javascripts, making the page responsive to input events sooner.
|
20
19
|
|
21
|
-
The
|
22
|
-
|
23
|
-
the
|
24
|
-
simply flush the rendering buffer from a helper method.
|
20
|
+
The main barrier to this in Rails is that layouts are rendered before the
|
21
|
+
content of the page. The control flow must thus be altered to render the page in
|
22
|
+
the order the client needs to receive it - layout first.
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
Template Streaming circumvents the template rendering order by introducing
|
29
|
-
*prelayouts*. A prelayout wraps a layout, and is rendered *before* the layout
|
30
|
-
and its content. By using the provided `flush` helper prior to yielding in the
|
31
|
-
prelayout, one can now output content early in the rendering process, giving
|
32
|
-
profiles that look more like:
|
24
|
+
With streaming, your profiles can look more like this:
|
33
25
|
|
34
26
|
![Progressive Rendering Profile][fast-profile]
|
35
27
|
|
36
|
-
[slow-profile]:
|
37
|
-
[fast-profile]:
|
28
|
+
[slow-profile]: https://github.com/oggy/template_streaming/raw/master/doc/slow-profile.png
|
29
|
+
[fast-profile]: https://github.com/oggy/template_streaming/raw/master/doc/fast-profile.png
|
30
|
+
|
31
|
+
## How
|
32
|
+
|
33
|
+
Just add the `template_streaming` gem to your application, and add a `stream`
|
34
|
+
call for the actions you'd like to stream. For example, to stream just the
|
35
|
+
`index` action of your `HomeController`, it would look like this:
|
36
|
+
|
37
|
+
class HomeController
|
38
|
+
stream :only => :index
|
39
|
+
|
40
|
+
def index
|
41
|
+
...
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
To stream everything, just add `stream` to your `ApplicationController`.
|
46
|
+
|
47
|
+
Now you may pepper `flush` calls strategically throughout your views to force a
|
48
|
+
flush, such as just after the stylesheet and javascript tags. `flush` may occur
|
49
|
+
in both templates and their layouts.
|
38
50
|
|
39
51
|
## API
|
40
52
|
|
@@ -48,9 +60,12 @@ This has several implications:
|
|
48
60
|
* Anything that needs to inspect or modify the body should be moved to a
|
49
61
|
middleware.
|
50
62
|
* Modifications to cookies (this includes the flash and session if using the
|
51
|
-
cookie store
|
52
|
-
|
53
|
-
|
63
|
+
cookie store) must not be made in the view. In fact, these objects will be
|
64
|
+
frozen when streaming.
|
65
|
+
* An exception during rendering cannot result in a 500 response, as the headers
|
66
|
+
will have already been sent. Instead, the innermost partial which contains an
|
67
|
+
error will simply render nothing, and error information is injected into the
|
68
|
+
foot of the page in development mode.
|
54
69
|
|
55
70
|
### Helpers
|
56
71
|
|
@@ -58,32 +73,76 @@ This has several implications:
|
|
58
73
|
client immediately.
|
59
74
|
* `push(data)` - send the given data to the client immediately.
|
60
75
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
76
|
+
## Support
|
77
|
+
|
78
|
+
Template Streaming currently only supports Rails 2.3.11. Rails 3.0 support is
|
79
|
+
planned in the near future. Rails 3.1 will ship with support for streaming. This
|
80
|
+
gem will be updated to meet the API of Rails 3.1 as it evolves, to help you
|
81
|
+
migrate.
|
82
|
+
|
83
|
+
Streaming also requires a web server that does not buffer Rack responses. It has
|
84
|
+
been tested **successfully** with [Passenger][passenger], [Unicorn][unicorn],
|
85
|
+
and [Mongrel][mongrel]. Note that Unicorn requires the `:tcp_nopush => false`
|
86
|
+
configuration option. [Thin][thin] is only supported if the
|
87
|
+
[Event Machine Flush][event-machine-flush] gem is installed. WEBrick does
|
88
|
+
**not** support streaming. [Please send me][contact] your experiences with other
|
66
89
|
web servers!
|
67
90
|
|
68
|
-
[mongrel]: http://github.com/fauna/mongrel
|
69
91
|
[passenger]: http://www.modrails.com
|
70
|
-
[
|
71
|
-
[
|
92
|
+
[unicorn]: http://unicorn.bogomips.org/
|
93
|
+
[mongrel]: https://github.com/fauna/mongrel
|
94
|
+
[thin]: https://github.com/macournoyer/thin
|
95
|
+
[event-machine-flush]: https://github.com/oggy/event_machine_flush
|
72
96
|
[contact]: mailto:george.ogata@gmail.com
|
73
97
|
|
74
98
|
### Controller
|
75
99
|
|
76
|
-
|
77
|
-
|
78
|
-
|
100
|
+
Class methods:
|
101
|
+
|
102
|
+
* `stream` - stream responses for these actions. Takes `:only` or `:except`
|
103
|
+
options, like `before_filter`.
|
104
|
+
|
105
|
+
* `when_streaming_template` - registers a callback to be called during `render`
|
106
|
+
when rendering progressively. This is before the body is rendered, or any
|
107
|
+
data is sent to the client.
|
108
|
+
|
109
|
+
Instance methods:
|
79
110
|
|
80
|
-
|
111
|
+
* `render` has been modified to accept a `:stream` option. If true, the
|
112
|
+
response will be streamed, otherwise it won't. This overrides the setting set
|
113
|
+
by the `stream` method above.
|
114
|
+
|
115
|
+
### Error Recovery
|
116
|
+
|
117
|
+
As mentioned above, headers are sent to the client before view rendering starts,
|
118
|
+
which means it's not possible to send an error response in the event of an
|
119
|
+
uncaught exception. Instead, the innermost template which raised the error
|
120
|
+
simply renders nothing. This has the added advantage of minimizing the impact on
|
121
|
+
your visitors, as the rest of the page will render fine.
|
122
|
+
|
123
|
+
When an error is swallowed like this, it is passed to an error hander callback,
|
124
|
+
which you can set as follows.
|
125
|
+
|
126
|
+
TemplateStreaming.on_streaming_error do |controller, exception|
|
127
|
+
...
|
128
|
+
end
|
129
|
+
|
130
|
+
This is where you should hook in your error notification system. Errors are also
|
131
|
+
logged to the application log.
|
132
|
+
|
133
|
+
In addition, in development mode, error information is injected into the foot of
|
134
|
+
the page. This is presented over the top of the rendered page, so the result
|
135
|
+
looks much like when not streaming.
|
136
|
+
|
137
|
+
## Streaming Templates Effectively
|
81
138
|
|
82
139
|
Conventional wisdom says to put your external stylesheets in the HEAD of your
|
83
140
|
page, and your external javascripts at the bottom of the BODY (markup in
|
84
141
|
[HAML][haml]):
|
85
142
|
|
86
|
-
|
143
|
+
[haml]: http://haml-lang.com
|
144
|
+
|
145
|
+
### `app/views/layouts/application.html.haml`
|
87
146
|
|
88
147
|
!!! 5
|
89
148
|
%html
|
@@ -91,65 +150,101 @@ page, and your external javascripts at the bottom of the BODY (markup in
|
|
91
150
|
= stylesheet_link_tag 'one'
|
92
151
|
= stylesheet_link_tag 'two'
|
93
152
|
- flush
|
94
|
-
|
153
|
+
%body
|
154
|
+
= yield
|
155
|
+
= javascript_include_tag 'one'
|
156
|
+
= javascript_include_tag 'two'
|
157
|
+
|
158
|
+
When streaming, however, you can do better: put the javascripts at the top of
|
159
|
+
the page too, and fetch them *asynchronously*. This can be done by appending a
|
160
|
+
script tag to the HEAD of the page in a small piece of inline javascript:
|
95
161
|
|
96
162
|
### `app/views/layouts/application.html.haml`
|
97
163
|
|
164
|
+
!!! 5
|
165
|
+
%html
|
166
|
+
%head
|
167
|
+
= stylesheet_link_tag 'one'
|
168
|
+
= stylesheet_link_tag 'two'
|
169
|
+
= javascript_tag do
|
170
|
+
= File.read(Rails.public_path + '/javascripts/get_script.js')
|
171
|
+
$.getScript('#{javascript_path('jquery')}');
|
172
|
+
$.getScript('#{javascript_path('application')}');
|
98
173
|
%body
|
99
|
-
|
100
|
-
|
101
|
-
|
174
|
+
- flush
|
175
|
+
= yield
|
176
|
+
|
177
|
+
### `public/javascripts/get_script.js`
|
178
|
+
|
179
|
+
//
|
180
|
+
// Credit: Sam Cole [https://gist.github.com/364746]
|
181
|
+
//
|
182
|
+
window.$ = {
|
183
|
+
getScript: function(script_src, callback) {
|
184
|
+
var done = false;
|
185
|
+
var head = document.getElementsByTagName("head")[0] || document.documentElement;
|
186
|
+
var script = document.createElement("script");
|
187
|
+
script.src = script_src;
|
188
|
+
script.onload = script.onreadystatechange = function() {
|
189
|
+
if ( !done && (!this.readyState ||
|
190
|
+
this.readyState === "loaded" || this.readyState === "complete") ) {
|
191
|
+
if(callback) callback();
|
102
192
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
193
|
+
// Handle memory leak in IE
|
194
|
+
script.onload = script.onreadystatechange = null;
|
195
|
+
if ( head && script.parentNode ) {
|
196
|
+
head.removeChild( script );
|
197
|
+
}
|
108
198
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
199
|
+
done = true;
|
200
|
+
}
|
201
|
+
};
|
202
|
+
head.insertBefore( script, head.firstChild );
|
203
|
+
}
|
204
|
+
};
|
205
|
+
|
206
|
+
If you have inline javascript that depends on the fetched scripts, you'll need
|
207
|
+
to delay its execution until the scripts have been run. You can do this by
|
208
|
+
wrapping the javascript in a function, with a guard which will delay execution
|
209
|
+
until the script is loaded, unless the script has already been loaded. Example:
|
114
210
|
|
115
|
-
###
|
211
|
+
### Layout
|
116
212
|
|
117
213
|
!!! 5
|
118
214
|
%html
|
119
215
|
%head
|
120
|
-
= define_get_script
|
121
216
|
= stylesheet_link_tag 'one'
|
122
217
|
= stylesheet_link_tag 'two'
|
123
|
-
=
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
218
|
+
= javascript_tag do
|
219
|
+
= File.read(Rails.public_path + '/javascripts/get_script.js')
|
220
|
+
$.getScript('#{javascript_path('jquery')}', function() {
|
221
|
+
window.script_loaded = 1;
|
222
|
+
|
223
|
+
// If the inline code has been loaded (but not yet run), run it
|
224
|
+
// now. Otherwise, it will be run immediately when it's available.
|
225
|
+
if (window.inline)
|
226
|
+
inline();
|
227
|
+
});
|
130
228
|
%body
|
131
|
-
|
229
|
+
- flush
|
230
|
+
= yield
|
132
231
|
|
133
|
-
###
|
232
|
+
### View
|
134
233
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
end
|
140
|
-
end
|
234
|
+
- javascript_tag do
|
235
|
+
window.inline() {
|
236
|
+
// ... inline javascript code ...
|
237
|
+
}
|
141
238
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
end
|
147
|
-
end
|
239
|
+
// If the script is already loaded, run it now. Otherwise, the callback
|
240
|
+
// above will run it after the script is loaded.
|
241
|
+
if (window.script_loaded)
|
242
|
+
inline();
|
148
243
|
|
149
|
-
### `public/javascripts/get_script.js`
|
244
|
+
### In `public/javascripts/get_script.js`
|
150
245
|
|
151
246
|
//
|
152
|
-
//
|
247
|
+
// Credit: Sam Cole [https://gist.github.com/364746]
|
153
248
|
//
|
154
249
|
window.$ = {
|
155
250
|
getScript: function(script_src, callback) {
|
@@ -175,20 +270,14 @@ script asynchronously, and then appends the script tag to the HEAD.
|
|
175
270
|
}
|
176
271
|
};
|
177
272
|
|
178
|
-
|
179
|
-
|
180
|
-
[haml]: http://haml-lang.com
|
181
|
-
[stefanov]: http://www.yuiblog.com/blog/2008/07/22/non-blocking-scripts
|
182
|
-
[get-script]: http://gist.github.com/364746
|
183
|
-
|
184
|
-
## Note on Patches/Pull Requests
|
273
|
+
## Contributing
|
185
274
|
|
186
|
-
* Bug reports
|
187
|
-
* Source
|
275
|
+
* [Bug reports](https://github.com/oggy/template_streaming/issues)
|
276
|
+
* [Source](https://github.com/oggy/template_streaming)
|
188
277
|
* Patches: Fork on Github, send pull request.
|
189
|
-
*
|
278
|
+
* Include tests where practical.
|
190
279
|
* Leave the version alone, or bump it in a separate commit.
|
191
280
|
|
192
281
|
## Copyright
|
193
282
|
|
194
|
-
Copyright (c)
|
283
|
+
Copyright (c) George Ogata. See LICENSE for details.
|
data/Rakefile
CHANGED
@@ -1,22 +1 @@
|
|
1
|
-
gem 'ritual'
|
2
1
|
require 'ritual'
|
3
|
-
|
4
|
-
spec_task :spec do |t|
|
5
|
-
t.libs << 'lib' << 'spec'
|
6
|
-
t.spec_files = FileList['spec/**/*_spec.rb']
|
7
|
-
end
|
8
|
-
|
9
|
-
spec_task :rcov do |t|
|
10
|
-
t.libs << 'lib' << 'spec'
|
11
|
-
t.pattern = 'spec/**/*_spec.rb'
|
12
|
-
t.rcov = true
|
13
|
-
end
|
14
|
-
|
15
|
-
rdoc_task do |t|
|
16
|
-
t.rdoc_dir = 'rdoc'
|
17
|
-
t.title = "Template Streaming #{version}"
|
18
|
-
t.rdoc_files.include('README*')
|
19
|
-
t.rdoc_files.include('lib/**/*.rb')
|
20
|
-
end
|
21
|
-
|
22
|
-
task :default => :spec
|
data/lib/template_streaming.rb
CHANGED
@@ -5,43 +5,17 @@ module TemplateStreaming
|
|
5
5
|
send "#{key}=", value
|
6
6
|
end
|
7
7
|
end
|
8
|
-
|
9
|
-
#
|
10
|
-
# If true, always reference the flash before returning from the
|
11
|
-
# action when rendering progressively.
|
12
|
-
#
|
13
|
-
# This is required for the flash to work with progressive
|
14
|
-
# rendering, but unlike standard Rails behavior, will cause the
|
15
|
-
# flash to be swept even if it's never referenced in the
|
16
|
-
# views. This usually isn't an issue, as flash messages are
|
17
|
-
# typically rendered in the layout, causing a reference anyway.
|
18
|
-
#
|
19
|
-
# Default: true.
|
20
|
-
#
|
21
|
-
attr_accessor :autosweep_flash
|
22
|
-
|
23
|
-
#
|
24
|
-
# If true, always set the authenticity token before returning from
|
25
|
-
# the action when rendering progressively.
|
26
|
-
#
|
27
|
-
# This is required for the authenticity token to work with
|
28
|
-
# progressive rendering, but unlike standard Rails behavior, will
|
29
|
-
# cause the token to be set (and thus the session updated) even if
|
30
|
-
# it's never referenced in views.
|
31
|
-
#
|
32
|
-
# Default: true.
|
33
|
-
#
|
34
|
-
attr_accessor :set_authenticity_token
|
35
8
|
end
|
36
9
|
|
37
|
-
|
38
|
-
self.set_authenticity_token = true
|
10
|
+
STREAMING_KEY = 'template_streaming.streaming'.freeze
|
39
11
|
|
40
12
|
module Controller
|
41
13
|
def self.included(base)
|
42
14
|
base.class_eval do
|
15
|
+
extend ClassMethods
|
43
16
|
alias_method_chain :render, :template_streaming
|
44
17
|
alias_method_chain :render_to_string, :template_streaming
|
18
|
+
alias_method_chain :flash, :template_streaming
|
45
19
|
helper_method :flush, :push
|
46
20
|
|
47
21
|
include ActiveSupport::Callbacks
|
@@ -49,11 +23,39 @@ module TemplateStreaming
|
|
49
23
|
end
|
50
24
|
end
|
51
25
|
|
26
|
+
module ClassMethods
|
27
|
+
def stream(options={})
|
28
|
+
before_filter :action_streams, options
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def action_streams
|
33
|
+
@action_streams = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def action_streams?
|
37
|
+
@action_streams
|
38
|
+
end
|
39
|
+
|
52
40
|
def render_with_template_streaming(*args, &block)
|
53
|
-
|
54
|
-
|
41
|
+
options = args.first { |a| a.is_a?(Hash) }
|
42
|
+
if options && options.size == 1 && options.key?(:stream)
|
43
|
+
# Need to set the default values, since the standard #render won't.
|
44
|
+
options[:template] = default_template
|
45
|
+
options[:layout] = true
|
46
|
+
end
|
47
|
+
push_render_stack_frame do |stack_height|
|
48
|
+
if start_streaming_template?(stack_height, *args)
|
49
|
+
@streaming_template = true
|
50
|
+
@template.streaming_template = true
|
55
51
|
@performed_render = true
|
56
|
-
@streaming_body = StreamingBody.new(
|
52
|
+
@streaming_body = StreamingBody.new(template_streaming_threshold) do
|
53
|
+
cookies.freeze
|
54
|
+
if self.class.session_store.sent_with_headers?
|
55
|
+
session.freeze
|
56
|
+
flash.freeze
|
57
|
+
end
|
58
|
+
|
57
59
|
@performed_render = false
|
58
60
|
last_piece = render_without_template_streaming(*args, &block)
|
59
61
|
# The original render will clobber our response.body, so
|
@@ -62,33 +64,44 @@ module TemplateStreaming
|
|
62
64
|
end
|
63
65
|
response.body = @streaming_body
|
64
66
|
response.prepare!
|
65
|
-
|
66
|
-
form_authenticity_token if TemplateStreaming.set_authenticity_token
|
67
|
-
run_callbacks :when_streaming_template
|
67
|
+
form_authenticity_token # generate now
|
68
68
|
|
69
|
-
# Normally,
|
70
|
-
# means
|
71
|
-
#
|
72
|
-
#
|
73
|
-
# twice, obliterating its contents.
|
69
|
+
# Normally, the flash is swept on first reference. This
|
70
|
+
# means we need to ensure it's referenced before the session
|
71
|
+
# is persisted. In the case of the cookie store, that's when
|
72
|
+
# the headers are sent, so we force a reference now.
|
74
73
|
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
|
78
|
-
|
79
|
-
|
74
|
+
# But alas, that's not all. @_flash is removed after
|
75
|
+
# #perform_action, which means calling #flash in the view
|
76
|
+
# would cause the flash to be referenced again, sweeping the
|
77
|
+
# flash a second time. To prevent this, we preserve the
|
78
|
+
# flash in a separate ivar, and patch #flash to return this
|
79
|
+
# if we're streaming.
|
80
|
+
#
|
81
|
+
flash # ensure sweep
|
82
|
+
@template_streaming_flash = @_flash
|
83
|
+
request.env[STREAMING_KEY] = true
|
84
|
+
|
85
|
+
run_callbacks :when_streaming_template
|
80
86
|
else
|
81
87
|
render_without_template_streaming(*args, &block)
|
82
88
|
end
|
83
89
|
end
|
84
90
|
end
|
85
91
|
|
92
|
+
# Mark the case when it's a layout for a toplevel render. This is
|
93
|
+
# done here, as it's called after the option wrangling in
|
94
|
+
# AC::Base#render, and nowhere else.
|
95
|
+
def pick_layout(options)
|
96
|
+
result = super
|
97
|
+
options[:toplevel_render_with_layout] = true if result
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
86
101
|
# Override to ensure calling render_to_string from a helper
|
87
102
|
# doesn't trigger template streaming.
|
88
103
|
def render_to_string_with_template_streaming(*args, &block) # :nodoc
|
89
|
-
|
90
|
-
# top-level.
|
91
|
-
with_template_streaming_condition do
|
104
|
+
push_render_stack_frame do
|
92
105
|
render_to_string_without_template_streaming(*args, &block)
|
93
106
|
end
|
94
107
|
end
|
@@ -98,7 +111,7 @@ module TemplateStreaming
|
|
98
111
|
# immediately.
|
99
112
|
#
|
100
113
|
def flush
|
101
|
-
|
114
|
+
if @streaming_body && !@template.output_buffer.nil?
|
102
115
|
push @template.output_buffer.slice!(0..-1)
|
103
116
|
end
|
104
117
|
end
|
@@ -107,35 +120,50 @@ module TemplateStreaming
|
|
107
120
|
# Push the given data to the client immediately.
|
108
121
|
#
|
109
122
|
def push(data)
|
110
|
-
@streaming_body
|
111
|
-
|
123
|
+
if @streaming_body
|
124
|
+
@streaming_body.push(data)
|
125
|
+
flush_thin
|
126
|
+
end
|
112
127
|
end
|
113
128
|
|
114
129
|
def template_streaming_flash # :nodoc:
|
115
130
|
@template_streaming_flash
|
116
131
|
end
|
117
132
|
|
133
|
+
def streaming_template?
|
134
|
+
@streaming_template
|
135
|
+
end
|
136
|
+
|
118
137
|
private # --------------------------------------------------------
|
119
138
|
|
120
|
-
|
121
|
-
# Yield true if we should intercept this render call, false
|
122
|
-
# otherwise.
|
123
|
-
#
|
124
|
-
def with_template_streaming_condition(*args)
|
139
|
+
def push_render_stack_frame
|
125
140
|
@render_stack_height ||= 0
|
126
141
|
@render_stack_height += 1
|
127
142
|
begin
|
128
|
-
|
129
|
-
|
130
|
-
|
143
|
+
yield @render_stack_height
|
144
|
+
ensure
|
145
|
+
@render_stack_height -= 1
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def start_streaming_template?(render_stack_height, *render_args)
|
150
|
+
render_stack_height == 1 or
|
151
|
+
return false
|
152
|
+
|
153
|
+
return false if rendered_action_cache
|
131
154
|
|
132
|
-
|
133
|
-
|
155
|
+
(render_options = render_args.last).is_a?(Hash) or
|
156
|
+
render_options = {}
|
157
|
+
|
158
|
+
if !(UNSTREAMABLE_KEYS & render_options.keys).empty? || render_args.first == :update
|
159
|
+
false
|
160
|
+
else
|
161
|
+
explicit_option = render_options[:stream]
|
162
|
+
if explicit_option.nil?
|
163
|
+
action_streams?
|
134
164
|
else
|
135
|
-
|
165
|
+
explicit_option
|
136
166
|
end
|
137
|
-
ensure
|
138
|
-
@render_stack_height -= 1
|
139
167
|
end
|
140
168
|
end
|
141
169
|
|
@@ -145,7 +173,7 @@ module TemplateStreaming
|
|
145
173
|
# The number of bytes that must be received by the client before
|
146
174
|
# anything will be rendered.
|
147
175
|
#
|
148
|
-
def
|
176
|
+
def template_streaming_threshold
|
149
177
|
content_type = response.header['Content-type']
|
150
178
|
content_type.nil? || content_type =~ %r'\Atext/html' or
|
151
179
|
return 0
|
@@ -170,6 +198,15 @@ module TemplateStreaming
|
|
170
198
|
connection = request.env['template_streaming.thin_connection'] and
|
171
199
|
EventMachineFlush.flush(connection)
|
172
200
|
end
|
201
|
+
|
202
|
+
def flash_with_template_streaming # :nodoc:
|
203
|
+
if defined?(@template_streaming_flash)
|
204
|
+
# Flash has been swept - don't use the standard #flash or it'll sweep again.
|
205
|
+
@template_streaming_flash
|
206
|
+
else
|
207
|
+
flash_without_template_streaming
|
208
|
+
end
|
209
|
+
end
|
173
210
|
end
|
174
211
|
|
175
212
|
# Only prepare once.
|
@@ -196,53 +233,83 @@ module TemplateStreaming
|
|
196
233
|
|
197
234
|
module View
|
198
235
|
def self.included(base)
|
236
|
+
base.alias_method_chain :render, :template_streaming
|
199
237
|
base.alias_method_chain :_render_with_layout, :template_streaming
|
200
|
-
base.alias_method_chain :flash, :template_streaming
|
201
238
|
end
|
202
239
|
|
203
|
-
def
|
204
|
-
|
205
|
-
|
240
|
+
def render_with_template_streaming(*args, &block)
|
241
|
+
options = args.first
|
242
|
+
if streaming_template? && options.is_a?(Hash)
|
243
|
+
# These branches exist to handle the case where AC::Base#render calls
|
244
|
+
# AV::Base#render for rendering a partial with a layout. AC::Base
|
245
|
+
# renders the partial then the layout separately, but we need to render
|
246
|
+
# them together, in the reverse order (layout first). We do this by
|
247
|
+
# standard-rendering the layout with a block that renders the partial.
|
248
|
+
if options[:toplevel_render_with_layout] && (partial = options[:partial])
|
249
|
+
# Don't render yet - we need to do the layout first.
|
250
|
+
options.delete(:toplevel_render_with_layout)
|
251
|
+
return DeferredPartialRender.new(args)
|
252
|
+
elsif options[:text].is_a?(DeferredPartialRender)
|
253
|
+
render = options.delete(:text)
|
254
|
+
# We patch the case of rendering :partial with :layout while
|
255
|
+
# streaming in _render_with_layout.
|
256
|
+
return render(render.args.first.merge(:layout => options[:layout]))
|
257
|
+
end
|
206
258
|
end
|
259
|
+
render_without_template_streaming(*args, &block)
|
260
|
+
end
|
261
|
+
|
262
|
+
DeferredPartialRender = Struct.new(:args)
|
263
|
+
|
264
|
+
attr_writer :streaming_template
|
265
|
+
|
266
|
+
def streaming_template?
|
267
|
+
@streaming_template
|
207
268
|
end
|
208
269
|
|
209
|
-
def
|
210
|
-
if
|
270
|
+
def _render_with_layout_with_template_streaming(options, local_assigns, &block)
|
271
|
+
if !streaming_template?
|
272
|
+
_render_with_layout_without_template_streaming(options, local_assigns, &block)
|
273
|
+
elsif block_given?
|
274
|
+
# The standard method doesn't properly restore @_proc_for_layout. Do it ourselves.
|
275
|
+
original_proc_for_layout = @_proc_for_layout
|
211
276
|
begin
|
212
|
-
|
213
|
-
# nil out @_proc_for_layout else rendering with the layout will call it again.
|
214
|
-
@_proc_for_layout, original_proc_for_layout = nil, @_proc_for_layout
|
215
|
-
begin
|
216
|
-
block.call
|
217
|
-
ensure
|
218
|
-
@_proc_for_layout = original_proc_for_layout
|
219
|
-
end
|
220
|
-
end
|
221
|
-
render(:file => prelayout, :locals => locals)
|
277
|
+
_render_with_layout_without_template_streaming(options, local_assigns, &block)
|
222
278
|
ensure
|
223
|
-
@_proc_for_layout =
|
279
|
+
@_proc_for_layout = original_proc_for_layout
|
280
|
+
end
|
281
|
+
elsif options[:layout].is_a?(ActionView::Template)
|
282
|
+
# Toplevel render call, from the controller.
|
283
|
+
layout = options.delete(:layout)
|
284
|
+
with_render_proc_for_layout(options) do
|
285
|
+
render(options.merge(:file => layout.path_without_format_and_extension))
|
224
286
|
end
|
225
287
|
else
|
226
|
-
|
288
|
+
layout = options.delete(:layout)
|
289
|
+
with_render_proc_for_layout(options) do
|
290
|
+
if (options[:inline] || options[:file] || options[:text])
|
291
|
+
render(:file => layout, :locals => local_assigns)
|
292
|
+
else
|
293
|
+
render(options.merge(:partial => layout))
|
294
|
+
end
|
295
|
+
end
|
227
296
|
end
|
228
297
|
end
|
229
298
|
|
230
|
-
def
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
# Override ActionView::Base#flash to prevent a double-sweep.
|
245
|
-
controller.instance_eval { @template_streaming_flash || flash }
|
299
|
+
def with_render_proc_for_layout(options)
|
300
|
+
original_proc_for_layout = @_proc_for_layout
|
301
|
+
@_proc_for_layout = lambda do |*args|
|
302
|
+
if args.empty?
|
303
|
+
render(options)
|
304
|
+
else
|
305
|
+
instance_variable_get(:"@content_for_#{args.first}")
|
306
|
+
end
|
307
|
+
end
|
308
|
+
begin
|
309
|
+
yield
|
310
|
+
ensure
|
311
|
+
@_proc_for_layout = original_proc_for_layout
|
312
|
+
end
|
246
313
|
end
|
247
314
|
end
|
248
315
|
|
@@ -275,10 +342,24 @@ module TemplateStreaming
|
|
275
342
|
end
|
276
343
|
end
|
277
344
|
|
345
|
+
module AbstractSessionStoreExtension
|
346
|
+
def sent_with_headers?
|
347
|
+
false
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
module CookieSessionStoreExtension
|
352
|
+
def sent_with_headers?
|
353
|
+
true
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
278
357
|
ActionView::Base.send :include, View
|
279
358
|
ActionController::Base.send :include, Controller
|
280
359
|
ActionController::Response.send :include, Response
|
281
360
|
ActionController::Dispatcher.middleware.insert 0, Rack::Chunked
|
361
|
+
ActionController::Session::AbstractStore.extend AbstractSessionStoreExtension
|
362
|
+
ActionController::Session::CookieStore.extend CookieSessionStoreExtension
|
282
363
|
end
|
283
364
|
|
284
365
|
# Please let there be a better way to do this...
|
@@ -311,3 +392,7 @@ if defined?(Thin)
|
|
311
392
|
end
|
312
393
|
end
|
313
394
|
end
|
395
|
+
|
396
|
+
require 'template_streaming/error_recovery'
|
397
|
+
require 'template_streaming/caching'
|
398
|
+
require 'template_streaming/autoflushing'
|