Skip to content

Commit

Permalink
Added HTTP 1.1 streaming support
Browse files Browse the repository at this point in the history
  • Loading branch information
eugeniobruno committed Jul 31, 2020
1 parent ae0b3d0 commit f2b4203
Show file tree
Hide file tree
Showing 2 changed files with 286 additions and 0 deletions.
81 changes: 81 additions & 0 deletions lib/rasti/web/render.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ def view(template, locals={}, layout_template=nil)
layout(layout_template) { view_context.render template, locals }
end

def char_stream(chars, headers={}, chunk_size=5000)
stream(headers, chunk_size) do |streamer|
streamer.stream_chars(chars)
end
end

def json_object_stream(json_object, headers={}, chunk_size=5000)
json_stream(headers, chunk_size) do |streamer|
streamer.stream_json_object(json_object)
end
end

def json_array_stream(json_objects, headers={}, chunk_size=5000)
json_stream(headers, chunk_size) do |streamer|
streamer.stream_json_array(json_objects)
end
end

private

def respond_with(status, headers, body)
Expand All @@ -92,6 +110,69 @@ def extract_body(args)
args.detect { |a| a.is_a? String }
end

def stream(headers={}, chunk_size=5000)
headers.merge(headers_for_streaming).each do |k, v|
response[k] = v
end

response.body = Enumerator.new do |yielder|
yield Streamer.new(yielder, chunk_size)
end
end

def json_stream(headers={}, chunk_size=5000, &block)
stream(headers.merge(headers_for_json), chunk_size, &block)
end

def headers_for_json
{
'Content-Type' => 'application/json; charset=utf-8'
}
end

def headers_for_streaming
{
'X-Accel-Buffering' => 'no', # Stop NGINX from buffering
'Cache-Control' => 'no-cache' # Stop downstream caching
}
end

class Streamer
def initialize(stream, chunk_size)
@stream = stream
@chunk_size = chunk_size
end

def stream_chars(chars_or_string)
chars = chars_or_string.is_a?(String) ? chars_or_string.each_char : chars_or_string
chars.each_slice(chunk_size).each do |cs|
stream << cs.join
end
end

def stream_json_object(json_object)
json_string = json_object.is_a?(String) ? json_object : JSON.dump(json_object)
stream_chars(json_string)
end

def stream_json_array(json_objects)
is_first = true

json_objects.each do |json_object|
json_string = json_object.is_a?(String) ? json_object : JSON.dump(json_object)
prefix = is_first ? '[' : ','
stream_chars("#{prefix}#{json_string}")
is_first = false
end

missing_chars = is_first ? '[]' : ']'
stream_chars(missing_chars)
end

private

attr_reader :stream, :chunk_size
end
end
end
end
205 changes: 205 additions & 0 deletions spec/render_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,209 @@

end

describe 'Char stream' do

let(:string) { 'Imagine this is a multi-megabyte text.' }

it 'Empty' do
render.char_stream ''

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal []
end

it 'Body' do
render.char_stream string

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
string
]
end

it 'Body and headers' do
render.char_stream string, 'Content-Type' => 'text/html; charset=utf-8'

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response['Content-Type'].must_equal 'text/html; charset=utf-8'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
string
]
end

it 'Body, headers and chunk size' do
render.char_stream string, {'Content-Type' => 'text/html; charset=utf-8'}, 10

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response['Content-Type'].must_equal 'text/html; charset=utf-8'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
'Imagine th',
'is is a mu',
'lti-megaby',
'te text.'
]
end

end

describe 'Json object stream' do

let(:object) { {id: 123, color: 'red', size: 'XXL'} }

it 'Empty' do
render.json_object_stream({})

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response['Content-Type'].must_equal 'application/json; charset=utf-8'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
{}.to_json
]
end

it 'Body' do
render.json_object_stream object

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response['Content-Type'].must_equal 'application/json; charset=utf-8'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
object.to_json
]
end

it 'Body and headers' do
render.json_object_stream object, 'Connection' => 'keep-alive'

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response['Content-Type'].must_equal 'application/json; charset=utf-8'
response['Connection'].must_equal 'keep-alive'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
object.to_json
]
end

it 'Body, headers and chunk size' do
render.json_object_stream object, {'Connection' => 'keep-alive'}, 10

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response['Content-Type'].must_equal 'application/json; charset=utf-8'
response['Connection'].must_equal 'keep-alive'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
'{"id":123,',
'"color":"r',
'ed","size"',
':"XXL"}'
]
end

end

describe 'Json array stream' do

let(:array) { [{id: 123, color: 'red', size: 'XXL'}, {id: 456, color: 'green', size: 'XXXL'}] }

it 'Empty' do
render.json_array_stream []

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response['Content-Type'].must_equal 'application/json; charset=utf-8'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
[].to_json
]
end

it 'Body' do
render.json_array_stream array

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response['Content-Type'].must_equal 'application/json; charset=utf-8'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
"[#{array[0].to_json}",
",#{array[1].to_json}",
"]"
]
end

it 'Body and headers' do
render.json_array_stream array, 'Connection' => 'keep-alive'

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response['Content-Type'].must_equal 'application/json; charset=utf-8'
response['Connection'].must_equal 'keep-alive'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
"[#{array[0].to_json}",
",#{array[1].to_json}",
"]"
]
end

it 'Body, headers and chunk size' do
render.json_array_stream array, {'Connection' => 'keep-alive'}, 10

response.status.must_equal 200
response['X-Accel-Buffering'] = 'no'
response['Cache-Control'] = 'no-cache'
response['Content-Type'].must_equal 'application/json; charset=utf-8'
response['Connection'].must_equal 'keep-alive'
response.body.must_be_instance_of Enumerator
chunks = response.body.to_a
chunks.must_equal [
'[{"id":123',
',"color":"',
'red","size',
'":"XXL"}',
',{"id":456',
',"color":"',
'green","si',
'ze":"XXXL"',
'}',
']'
]
end

end

end

0 comments on commit f2b4203

Please sign in to comment.