template_streaming 0.0.11 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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