Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to streaming/event source mechanism for live reload #458

Merged
merged 2 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bridgetown-core/lib/bridgetown-core/rack/roda.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Roda < ::Roda
plugin :json
plugin :json_parser
plugin :cookies
plugin :streaming
plugin :public, root: Bridgetown::Current.preloaded_configuration.destination
plugin :not_found do
output_folder = Bridgetown::Current.preloaded_configuration.destination
Expand Down
54 changes: 44 additions & 10 deletions bridgetown-core/lib/bridgetown-core/rack/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

module Bridgetown
module Rack
@interrupted = false

class << self
attr_accessor :interrupted
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use ActiveSupport's mattr_accessor here by any chance?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question…I think mattr_accessor is particularly useful when object instances which have included a module want to access that module's accessor easily. No need for that here, so probably wouldn't need the overhead

end

class Routes
class << self
attr_accessor :tracked_subclasses, :router_block
Expand Down Expand Up @@ -37,7 +43,7 @@ def merge(roda_app)
def start!(roda_app)
if Bridgetown.env.development? &&
!Bridgetown::Current.preloaded_configuration.skip_live_reload
setup_live_reload roda_app.request
setup_live_reload roda_app
end

Bridgetown::Rack::Routes.tracked_subclasses&.each_value do |klass|
Expand All @@ -51,15 +57,31 @@ def start!(roda_app)
nil
end

def setup_live_reload(request)
request.get "_bridgetown/live_reload" do
{
last_mod: File.stat(
File.join(Bridgetown::Current.preloaded_configuration.destination, "index.html")
).mtime.to_i,
}
rescue StandardError => e
{ last_mod: 0, error: e.message }
def setup_live_reload(app) # rubocop:disable Metrics/AbcSize
sleep_interval = 0.2
file_to_check = File.join(app.class.opts[:bridgetown_preloaded_config].destination,
"index.html")

app.request.get "_bridgetown/live_reload" do
app.response["Content-Type"] = "text/event-stream"

@_mod = File.exist?(file_to_check) ? File.stat(file_to_check).mtime.to_i : 0
app.stream async: true do |out|
# 5 second intervals so Puma's threads aren't all exausted
(5 / sleep_interval).to_i.times do
break if Bridgetown::Rack.interrupted

new_mod = File.exist?(file_to_check) ? File.stat(file_to_check).mtime.to_i : 0
if @_mod < new_mod
out << "data: reloaded!\n\n"
break
else
out << "data: #{new_mod}\n\n"
end

sleep sleep_interval
end
end
end
end
end
Expand All @@ -86,3 +108,15 @@ def respond_to_missing?(method_name, include_private = false)
end
end
end

if Bridgetown.env.development? &&
!Bridgetown::Current.preloaded_configuration.skip_live_reload
Puma::Launcher.class_eval do
alias_method :_old_stop, :stop
def stop
Bridgetown::Rack.interrupted = true

_old_stop
end
end
end
46 changes: 25 additions & 21 deletions bridgetown-core/lib/bridgetown-core/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -401,30 +401,34 @@ def live_reload_js(site) # rubocop:disable Metrics/MethodLength
return "" unless Bridgetown.env.development? && !site.config.skip_live_reload

code = <<~JAVASCRIPT
let first_mod = 0
let connection_errors = 0
const checkForReload = () => {
fetch("/_bridgetown/live_reload").then(response => {
if (response.ok) {
response.json().then(data => {
const last_mod = data.last_mod
if (first_mod === 0) {
first_mod = last_mod
} else if (last_mod > first_mod) {
location.reload()
}
setTimeout(() => checkForReload(), 700)
})
let lastmod = 0
function startReloadConnection() {
const evtSource = new EventSource("/_bridgetown/live_reload")
evtSource.onmessage = event => {
if (event.data == "reloaded!") {
location.reload()
} else {
if (connection_errors < 20) setTimeout(() => checkForReload(), 6000)
connection_errors++
const newmod = Number(event.data)
if (lastmod > 0 && newmod > 0 && lastmod < newmod) {
location.reload()
} else {
lastmod = newmod
}
}
}).catch((err) => {
if (connection_errors < 20) setTimeout(() => checkForReload(), 6000)
connection_errors++
})
}
evtSource.onerror = event => {
if (evtSource.readyState === 2) {
// reconnect with new object
evtSource.close()
console.warn("Live reload: attempting to reconnect in 3 seconds...")

setTimeout(() => startReloadConnection(), 3000)
}
}
}
checkForReload()
setTimeout(() => {
startReloadConnection()
}, 500)
JAVASCRIPT

%(<script type="module">#{code}</script>).html_safe
Expand Down