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.
@@ -0,0 +1,88 @@
1
+ module TemplateStreaming
2
+ class << self
3
+ #
4
+ # If non-nil, #flush will automatically be called when streaming
5
+ # before and after each render call.
6
+ #
7
+ # The value of this attribute should be a number, which is the
8
+ # number of milliseconds since the last flush that should elapse
9
+ # for the autoflush to occur. 0 means force a flush every time.
10
+ #
11
+ attr_accessor :autoflush
12
+ end
13
+
14
+ module Autoflushing
15
+ module View
16
+ def self.included(base)
17
+ base.alias_method_chain :render, :template_streaming_autoflushing
18
+ end
19
+
20
+ def render_with_template_streaming_autoflushing(*args, &block)
21
+ with_autoflushing do
22
+ render_without_template_streaming_autoflushing(*args, &block)
23
+ end
24
+ end
25
+
26
+ def capture(*args, &block)
27
+ if block == @_proc_for_layout
28
+ # Rendering the content of a streamed layout - inject autoflushing.
29
+ with_autoflushing do
30
+ super
31
+ end
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def with_autoflushing
38
+ controller.flush if TemplateStreaming.autoflush
39
+ fragment = yield
40
+ if TemplateStreaming.autoflush
41
+ controller.push(fragment)
42
+ ''
43
+ else
44
+ fragment
45
+ end
46
+ end
47
+ end
48
+
49
+ class Middleware
50
+ def initialize(app)
51
+ @app = app
52
+ end
53
+
54
+ def call(env)
55
+ response = @app.call(env)
56
+ if env[STREAMING_KEY] && TemplateStreaming.autoflush
57
+ response[2] = BodyProxy.new(response[2])
58
+ end
59
+ response
60
+ end
61
+
62
+ class BodyProxy
63
+ def initialize(body)
64
+ @body = body
65
+ end
66
+
67
+ def each
68
+ buffered_chunks = []
69
+ autoflush_due_at = Time.now.to_f
70
+ @body.each do |chunk|
71
+ buffered_chunks << chunk
72
+ if Time.now.to_f >= autoflush_due_at
73
+ yield buffered_chunks.join
74
+ buffered_chunks.clear
75
+ autoflush_due_at = Time.now.to_f + TemplateStreaming.autoflush
76
+ end
77
+ end
78
+ unless buffered_chunks.empty?
79
+ yield buffered_chunks.join
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ ActionView::Base.send :include, View
86
+ ActionController::Dispatcher.middleware.use Middleware
87
+ end
88
+ end
@@ -0,0 +1,68 @@
1
+ module TemplateStreaming
2
+ module Caching
3
+ CACHER_KEY = 'template_streaming.caching.cacher'.freeze
4
+
5
+ module Controller
6
+ def cache_page(content = nil, options = nil)
7
+ if content
8
+ super
9
+ else
10
+ request.env[CACHER_KEY] = lambda { |c| super(c, options) }
11
+ end
12
+ end
13
+ end
14
+
15
+ class Middleware
16
+ def initialize(app)
17
+ @app = app
18
+ end
19
+
20
+ def call(env)
21
+ response = @app.call(env)
22
+ path = env[CACHER_KEY] and
23
+ response[2] = CachingBodyProxy.new(response[2], env[CACHER_KEY])
24
+ response
25
+ end
26
+
27
+ class CachingBodyProxy
28
+ def initialize(body, cacher)
29
+ @body = body
30
+ @cacher = cacher
31
+ end
32
+
33
+ def each
34
+ chunks = []
35
+ @body.each do |chunk|
36
+ chunks << chunk
37
+ yield chunk
38
+ end
39
+ @cacher.call(chunks.join)
40
+ end
41
+ end
42
+ end
43
+
44
+ module ActionCacheFilter
45
+ def self.included(base)
46
+ base.alias_method_chain :after, :template_streaming_caching
47
+ end
48
+
49
+ def after_with_template_streaming_caching(controller)
50
+ if controller.streaming_template?
51
+ # This flag is ass-backwards to me. It really means *don't* cache the layout...
52
+ cache_layout? and
53
+ raise NotImplementedError, "sorry, using caches_action with :layout => false is not yet supported by Template Streaming"
54
+ controller.request.env[CACHER_KEY] = lambda do |content|
55
+ # This is what the standard method does.
56
+ controller.write_fragment(controller.action_cache_path.path, content, @options[:store_options])
57
+ end
58
+ else
59
+ after_without_template_streaming_caching(controller)
60
+ end
61
+ end
62
+ end
63
+
64
+ ActionController::Base.send :include, Controller
65
+ ActionController::Dispatcher.middleware.use Middleware
66
+ ActionController::Caching::Actions::ActionCacheFilter.send :include, ActionCacheFilter
67
+ end
68
+ end
@@ -1,119 +1,233 @@
1
1
  module TemplateStreaming
2
2
  module ErrorRecovery
3
- ENV_EXCEPTIONS = 'template_streaming.error_recovery.exceptions'.freeze
4
- ENV_SHOW_DETAILS = 'template_streaming.error_recovery.exceptions'.freeze
5
-
6
- module Rendering
7
- def uncaught_errors_html(errors)
8
- content = errors.map do |error|
9
- "<pre>#{uncaught_error_string(error)}</pre>"
10
- end.join
11
- <<-EOS.gsub(/^ *\|/, '')
12
- |<div style='position: absolute; left: 0px; top: 0px; background-color: #fff; z-index: 999'>
13
- | <h2 style="margin: 20px; font-weight: bold; border-bottom: 1px solid red">Rails Application Error</h2>
14
- | #{content}
15
- |</div>
16
- EOS
17
- end
3
+ CONTROLLER_KEY = 'template_streaming.error_recovery.controller'.freeze
4
+ EXCEPTIONS_KEY = 'template_streaming.error_recovery.exceptions'.freeze
18
5
 
19
- def uncaught_error_string(error)
20
- details = "#{error.class}: #{error.message}"
21
- backtrace = error.backtrace.join("\n").gsub(/^/, ' ')
22
- "<span style='font-weight: bold; margin: 20px'>#{h details}</span>\n#{h backtrace}"
6
+ class Middleware
7
+ def initialize(app)
8
+ @app = app
23
9
  end
24
- end
25
10
 
26
- module Controller
27
- def self.included(base)
28
- base.when_streaming_template :recover_from_errors
29
- base.helper Helper
30
- base.helper_method :recover_from_errors?
11
+ def call(env)
12
+ response = *@app.call(env)
13
+ if env[TemplateStreaming::STREAMING_KEY]
14
+ response[2] = BodyProxy.new(env, response[2])
15
+ response
16
+ else
17
+ response
18
+ end
31
19
  end
32
20
 
33
- def recover_from_errors
34
- @recover_from_errors = true
35
- end
21
+ class BodyProxy
22
+ def initialize(env, body)
23
+ @env = env
24
+ @body = body
25
+ @controller = @env[CONTROLLER_KEY]
26
+ end
36
27
 
37
- def recover_from_errors?
38
- @recover_from_errors
39
- end
40
- end
28
+ def each(&block)
29
+ if @controller && @controller.show_errors?
30
+ exceptions = @env[EXCEPTIONS_KEY] = []
31
+ @state = :start
32
+ @body.each do |chunk|
33
+ advance_state(chunk)
34
+ if !exceptions.empty?
35
+ try_to_insert_errors(chunk, exceptions)
36
+ end
37
+ yield chunk
38
+ end
39
+ if !exceptions.empty?
40
+ yield uninserted_errors(exceptions)
41
+ end
42
+ else
43
+ @body.each(&block)
44
+ end
45
+ end
41
46
 
42
- module Helper
43
- def render(*)
44
- begin
45
- super
46
- rescue ActionView::MissingTemplate => e
47
- # ActionView uses this as a signal to try another template engine.
48
- raise e
49
- rescue Exception => e
50
- raise e if !recover_from_errors?
51
- if HoptoadNotifier.configuration[:api_key]
52
- Rails.logger.error("#{e.class}: #{e.message}")
53
- Rails.logger.error(e.backtrace.join("\n").gsub(/^/, ' '))
54
- HoptoadNotifier.notify(e)
47
+ def advance_state(chunk, cursor=0)
48
+ case @state
49
+ when :start
50
+ if index = chunk.index(%r'<!doctype\b.*?>'i, cursor)
51
+ @state = :before_html
52
+ advance_state(chunk, index)
53
+ end
54
+ when :before_html
55
+ if index = chunk.index(%r'<html\b'i, cursor)
56
+ @state = :before_head
57
+ advance_state(chunk, index)
58
+ end
59
+ when :before_head
60
+ if index = chunk.index(%r'<head\b'i, cursor)
61
+ @state = :in_head
62
+ advance_state(chunk, index)
63
+ end
64
+ when :in_head
65
+ if index = chunk.index(%r'</head\b.*?>'i, cursor)
66
+ @state = :between_head_and_body
67
+ advance_state(chunk, index)
68
+ end
69
+ when :between_head_and_body
70
+ if index = chunk.index(%r'<body\b'i, cursor)
71
+ @state = :in_body
72
+ advance_state(chunk, index)
73
+ end
74
+ when :in_body
75
+ if index = chunk.index(%r'</body\b.*?>'i, cursor)
76
+ @state = :after_body
77
+ advance_state(chunk, index)
78
+ end
79
+ when :after_body
80
+ if index = chunk.index(%r'</html\b.*?>'i, cursor)
81
+ @state = :after_html
82
+ advance_state(chunk, index)
83
+ end
55
84
  end
85
+ end
56
86
 
57
- request.env[ENV_SHOW_DETAILS] = consider_all_requests_local || local_request?
87
+ def try_to_insert_errors(chunk, exceptions)
88
+ if (index = chunk =~ %r'</body\s*>\s*(?:</html\s*>\s*)?\z'im)
89
+ chunk.insert(index, render_exceptions(exceptions))
90
+ exceptions.clear
91
+ end
92
+ end
58
93
 
59
- # TODO: Find a way to make this suck less.
60
- is_template_error = e.is_a?(ActionView::TemplateError)
61
- if is_template_error && e.file_name =~ %r'\Aapp/views/prelayouts/'
62
- # Error in prelayout - no head or body rendered yet.
63
- head = "<head><title>Rails Application Error</title></head>"
64
- body = "<body>#{uncaught_errors_html([e])}</body>"
94
+ def uninserted_errors(exceptions)
95
+ html = render_exceptions(exceptions)
96
+ exceptions.clear
97
+ case @state
98
+ when :start
99
+ head = "<head><title>Unhandled Exception</title></head>"
100
+ body = "<body>#{html}</body>"
65
101
  "<!DOCTYPE html><html>#{head}#{body}</html>"
66
- elsif is_template_error && e.file_name =~ %r'\Aapp/views/layouts/'
67
- # Error in layout - unclosed head tag has been rendered.
68
- head = "<title>Rails Application Error</title></head>"
69
- body = "<body>#{uncaught_errors_html([e])}</body>"
102
+ when :before_html
103
+ head = "<head><title>Unhandled Exception</title></head>"
104
+ body = "<body>#{html}</body>"
105
+ "<html>#{head}#{body}</html>"
106
+ when :before_head
107
+ head = "<head><title>Unhandled Exception</title></head>"
108
+ body = "<body>#{html}</body>"
70
109
  "#{head}#{body}</html>"
71
- else
72
- # Body is being rendered - return nothing for this render
73
- # call, and render the exception in the middleware.
74
- request.env[ENV_EXCEPTIONS] << e
75
- ''
110
+ when :in_head
111
+ "</head><body>#{html}</body></html>"
112
+ when :between_head_and_body
113
+ "<body>#{html}</body></html>"
114
+ when :in_body
115
+ "#{html}</body></html>"
116
+ when :after_body
117
+ # Errors aren't likely to happen at this point, as after the body
118
+ # there should only be "</html>". Just stick our error html in there
119
+ # - it's invalid HTML no matter what we do.
120
+ "#{html}</html>"
121
+ when :after_html
122
+ html
76
123
  end
77
124
  end
78
- end
79
125
 
80
- include Rendering
126
+ def render_exceptions(exceptions)
127
+ template = @controller.response.template
128
+ template.render_streaming_exceptions(exceptions)
129
+ end
130
+ end
81
131
  end
82
132
 
83
- class Middleware
84
- def initialize(app)
85
- @app = app
133
+ module Controller
134
+ def self.included(base)
135
+ base.when_streaming_template :set_template_streaming_controller
136
+ base.class_inheritable_accessor :streaming_error_callbacks
137
+ base.class_inheritable_accessor :streaming_error_renderer
138
+ base.streaming_error_callbacks = []
139
+ base.extend ClassMethods
86
140
  end
87
141
 
88
- def call(env)
89
- @env = env
90
- env[ENV_EXCEPTIONS] = []
91
- status, headers, @body = *@app.call(env)
92
- [status, headers, self]
142
+ module ClassMethods
143
+ #
144
+ # Call the given block when an error occurs while streaming.
145
+ #
146
+ # The block is called with the controller instance and exception object.
147
+ #
148
+ # Hook in your exception notification system here.
149
+ #
150
+ def on_streaming_error(&block)
151
+ streaming_error_callbacks << block
152
+ end
153
+
154
+ #
155
+ # Call the give block to render errors injected into the page, when
156
+ # uncaught exceptions are raised while streaming.
157
+ #
158
+ # The block is called with the view instance an list of exception
159
+ # objects. It should return the HTML to inject into the page.
160
+ #
161
+ def render_streaming_errors_with(&block)
162
+ self.streaming_error_renderer = block
163
+ end
164
+ end
165
+
166
+ def set_template_streaming_controller
167
+ request.env[CONTROLLER_KEY] = self
168
+ end
169
+
170
+ def show_errors?
171
+ local_request?
172
+ end
173
+ end
174
+
175
+ module View
176
+ def self.included(base)
177
+ base.class_eval do
178
+ alias_method_chain :render, :template_streaming_error_recovery
179
+ end
93
180
  end
94
181
 
95
- def each
96
- # Assume there are no faux occurrences of </body>.
97
- @body.each do |chunk|
98
- if render_errors? && (chunk =~ %r'</body\b')
99
- errors = @env[ENV_EXCEPTIONS]
100
- chunk.insert($~.begin(0), uncaught_errors_html(errors))
182
+ def render_with_template_streaming_error_recovery(*args, &block)
183
+ if streaming_template?
184
+ begin
185
+ render_without_template_streaming_error_recovery(*args, &block)
186
+ rescue ActionView::MissingTemplate => e
187
+ # ActionView uses this as a signal to try another template format.
188
+ raise e
189
+ rescue Exception => e
190
+ logger.error "#{e.class}: #{e.message}"
191
+ logger.error e.backtrace.join("\n").gsub(/^/, ' ')
192
+ controller.streaming_error_callbacks.each{|c| c.call(e)}
193
+ exceptions = controller.request.env[EXCEPTIONS_KEY] and
194
+ exceptions << e
195
+ ''
101
196
  end
102
- yield chunk
197
+ else
198
+ render_without_template_streaming_error_recovery(*args, &block)
103
199
  end
104
200
  end
105
201
 
106
- private
202
+ def render_streaming_exceptions(exceptions)
203
+ controller.streaming_error_renderer.call(self, exceptions)
204
+ end
107
205
 
108
- def render_errors?
109
- !Rails.env.production? && !@env[ENV_EXCEPTIONS].empty?
206
+ def render_default_streaming_exceptions(exceptions)
207
+ # Ensure errors in the error rendering don't recurse.
208
+ @rendering_default_streaming_exceptions = true
209
+ begin
210
+ @content = exceptions.map do |exception|
211
+ template_path = ActionController::Rescue::RESCUES_TEMPLATE_PATH
212
+ @exception = exception
213
+ @rescues_path = template_path
214
+ render :file => "#{template_path}/rescues/template_error.erb"
215
+ end.join
216
+ render :file => "#{File.dirname(__FILE__)}/templates/errors.erb"
217
+ ensure
218
+ @rendering_default_streaming_exceptions = false
219
+ end
110
220
  end
221
+ end
111
222
 
112
- include Rendering
223
+ DEFAULT_ERROR_RENDERER = lambda do |view, exceptions|
224
+ view.render_default_streaming_exceptions(exceptions)
113
225
  end
114
226
 
115
- ActionController::Base.send(:include, Controller)
116
- ActionView::Base.send(:include, Helper)
117
- ActionController::Dispatcher.middleware.insert_after('ActionController::Failsafe', Middleware)
227
+ ActionController::Dispatcher.middleware.insert_after ActionController::Failsafe, Middleware
228
+ ActionController::Base.send :include, Controller
229
+ ActionView::Base.send :include, View
230
+
231
+ ActionController::Base.streaming_error_renderer = DEFAULT_ERROR_RENDERER
118
232
  end
119
233
  end