diff --git a/bridgetown-core/lib/bridgetown-core/rack/roda.rb b/bridgetown-core/lib/bridgetown-core/rack/roda.rb index 973186d57..4c29ef44f 100644 --- a/bridgetown-core/lib/bridgetown-core/rack/roda.rb +++ b/bridgetown-core/lib/bridgetown-core/rack/roda.rb @@ -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 diff --git a/bridgetown-core/lib/bridgetown-core/rack/routes.rb b/bridgetown-core/lib/bridgetown-core/rack/routes.rb index 28c09a3fb..a2bd83460 100644 --- a/bridgetown-core/lib/bridgetown-core/rack/routes.rb +++ b/bridgetown-core/lib/bridgetown-core/rack/routes.rb @@ -2,6 +2,12 @@ module Bridgetown module Rack + @interrupted = false + + class << self + attr_accessor :interrupted + end + class Routes class << self attr_accessor :tracked_subclasses, :router_block @@ -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| @@ -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 @@ -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 diff --git a/bridgetown-core/lib/bridgetown-core/utils.rb b/bridgetown-core/lib/bridgetown-core/utils.rb index f988b188b..06adf0a09 100644 --- a/bridgetown-core/lib/bridgetown-core/utils.rb +++ b/bridgetown-core/lib/bridgetown-core/utils.rb @@ -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 %().html_safe