Skip to content

Commit

Permalink
Add support for streaming the Phlex view
Browse files Browse the repository at this point in the history
  • Loading branch information
benpickles committed Dec 13, 2023
1 parent fbd0b41 commit 4b2af3f
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 5 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in phlex-sinatra.gemspec
gemspec

gem 'capybara'
gem 'puma'
gem 'rack-test'
gem 'rake'
gem 'rspec'
Expand Down
53 changes: 51 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,56 @@ get '/foo' do
end
```

## Why?
## Streaming

Streaming a Phlex view can be enabled by passing `stream: true` which will cause Phlex to automatically write to the response after the closing `</head>` and buffer the remaining content:

```ruby
get '/foo' do
phlex MyView.new, stream: true
end
```

Even with no further intervention this small change means that the browser will receive the complete `<head>` as quickly as possible and can start fetching and processing its external resources while waiting for the rest of the page to download.

You can also manually flush the contents of the buffer at any point using Phlex's `#flush` method:

```ruby
class Layout < Phlex::HTML
def template(&)
doctype
html {
head {
# All the usual stuff: links to external stylesheets and JavaScript etc.
}
# Phlex will automatically flush to the response at this point which will
# benefit all pages that opt in to streaming.
body {
# Standard site header and navigation.
render Header.new

yield_content(&)
}
}
end
end

class MyView < Phlex::HTML
def template
render Layout.new {
# Knowing that this page can take a while to generate we can choose to
# flush here so the browser can render the site header while downloading
# the rest of the page - which should help minimise the First Contentful
# Paint metric.
flush

# The rest of the big long page...
}
end
end
```

## Why do I need Sinatra's `url()` helper?

It might not seem obvious at first why you'd use `url()` at all given that you mostly just pass the string you want to output and then probably `false` so the scheme/host isn't included.

Expand All @@ -54,7 +103,7 @@ There are a couple of reasons:

2. **Awareness that the app is being served from a subdirectory**

This isn't something you encounter very often in a standard Sinatra app but you hit it quite quickly if you're using [Parklife](https://github.com/benpickles/parklife) to generate a static build which you host on GitHub Pages – which is exactly what prompted me to write this integration.
This isn't something you encounter very often in a standard Sinatra app but you hit it quite quickly if you're using [Parklife](https://github.com/benpickles/parklife) to generate a static build hosted on GitHub Pages – which is exactly what prompted me to write this integration.

In this case by using the `url()` helper you won’t have to change anything when switching between serving the app from `/` in development and hosting it at `/my-repository/` in production – internal links to other pages/stylesheets/etc will always be correct regardless.

Expand Down
10 changes: 8 additions & 2 deletions lib/phlex-sinatra.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,19 @@ class SGML

module Sinatra
module Templates
def phlex(obj, content_type: nil)
def phlex(obj, content_type: nil, stream: false)
raise Phlex::Sinatra::TypeError.new(obj) unless obj.is_a?(Phlex::SGML)

content_type ||= :svg if obj.is_a?(Phlex::SVG)
self.content_type(content_type) if content_type

obj.call(view_context: self)
if stream
self.stream do |out|
obj.call(out, view_context: self)
end
else
obj.call(view_context: self)
end
end
end
end
2 changes: 1 addition & 1 deletion phlex-sinatra.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']

spec.add_dependency 'phlex'
spec.add_dependency 'phlex', '>= 1.7.0'
end
54 changes: 54 additions & 0 deletions spec/phlex/sinatra_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ def template
end
end

class StreamingView < Phlex::HTML
def template
html {
head {
title { 'Streaming' }
}
body {
p { 1 }
flush # Internal private Phlex method.
p { 2 }
}
}
end
end

class SvgElem < Phlex::SVG
def template
svg { rect(width: 100, height: 100) }
Expand Down Expand Up @@ -60,6 +75,10 @@ class TestApp < Sinatra::Application
phlex MoreDetailsView.new
end

get '/stream' do
phlex StreamingView.new, stream: true
end

get '/svg' do
phlex SvgElem.new
end
Expand All @@ -73,6 +92,21 @@ class TestApp < Sinatra::Application
end
end

# Trick Capybara into managing Puma for us.
class NeedsServerDriver < Capybara::Driver::Base
def needs_server?
true
end
end

Capybara.register_driver :needs_server do
NeedsServerDriver.new
end

Capybara.app = TestApp
Capybara.default_driver = :needs_server
Capybara.server = :puma, { Silent: true }

RSpec.describe Phlex::Sinatra do
include Rack::Test::Methods

Expand Down Expand Up @@ -167,4 +201,24 @@ def app
expect(last_response.media_type).to eql('text/html')
end
end

context 'when streaming' do
def get2(path)
Net::HTTP.start(
Capybara.current_session.server.host,
Capybara.current_session.server.port,
) { |http|
http.get(path)
}
end

it 'outputs the full response' do
last_response = get2('/stream')

expect(last_response.body).to eql('<html><head><title>Streaming</title></head><body><p>1</p><p>2</p></body></html>')

# Indicates that Sinatra's streaming is being used.
expect(last_response['Content-Length']).to be_nil
end
end
end
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require 'capybara/rspec'
require 'phlex-sinatra'
require 'rack/test'
require 'sinatra/base'
Expand Down

0 comments on commit 4b2af3f

Please sign in to comment.