rails_live_reload 0.1.2 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +10 -10
- data/lib/rails_live_reload/config.rb +23 -3
- data/lib/rails_live_reload/javascript/long_polling.js +6 -0
- data/lib/rails_live_reload/javascript/polling.js +6 -0
- data/lib/rails_live_reload/javascript/websocket.js +6 -0
- data/lib/rails_live_reload/middleware/base.rb +75 -0
- data/lib/rails_live_reload/middleware/long_polling.rb +45 -0
- data/lib/rails_live_reload/middleware/polling.rb +19 -0
- data/lib/rails_live_reload/middleware/web_socket.rb +51 -0
- data/lib/rails_live_reload/version.rb +1 -1
- data/lib/rails_live_reload/watcher.rb +1 -0
- data/lib/rails_live_reload/web_socket/base.rb +116 -0
- data/lib/rails_live_reload/web_socket/client_socket.rb +153 -0
- data/lib/rails_live_reload/web_socket/event_loop.rb +131 -0
- data/lib/rails_live_reload/web_socket/message_buffer.rb +53 -0
- data/lib/rails_live_reload/web_socket/stream.rb +109 -0
- data/lib/rails_live_reload/web_socket/wrapper.rb +27 -0
- data/lib/rails_live_reload.rb +20 -6
- metadata +20 -39
- data/lib/rails_live_reload/client.rb +0 -75
- data/lib/rails_live_reload/rails/middleware/base.rb +0 -63
- data/lib/rails_live_reload/rails/middleware/long_polling.rb +0 -51
- data/lib/rails_live_reload/rails/middleware/polling.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 999e453ba10fc303a5f317609ad52abbb318972712802ea87606f17c89c2054a
|
4
|
+
data.tar.gz: 456e9df00f34d48e75a8cdbee655e035e68a0c98c468020946dc3ec6903848bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d3e4ddad3250f1cb40b06d0f17c69b537d25c26ebe90b3844f942540af27286c9d2f66d4a29562d2ffd9c725d0286b028da8a645ad43126ac57099a13b567bd
|
7
|
+
data.tar.gz: a7ac90c951252374f0d6cd357d822c33d8c2a330f2b2b487f3e048551b439168474a4da4dd822228644ea7d1cfdbfdc04e5064efab9957c6a55c8a455b1f1d1f
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
|
7
7
|
This is the simplest and probably the most robust way to add live reloading to your Rails app.
|
8
8
|
|
9
|
-
Just add the gem and thats it, now you have a live reloading.
|
9
|
+
Just add the gem and thats it, now you have a live reloading. **You don't need anything other than this gem for live reloading to work**.
|
10
10
|
|
11
11
|
Works with:
|
12
12
|
|
@@ -40,9 +40,12 @@ $ bundle
|
|
40
40
|
|
41
41
|
## Configuration
|
42
42
|
|
43
|
-
### There are
|
44
|
-
1. `:
|
45
|
-
2. `:
|
43
|
+
### There are three modes:
|
44
|
+
1. `:websocket` - This is a default mode which uses websockets to trigger page reloading.
|
45
|
+
2. `:long_polling` - **[Deprecated]** This mode uses [long polling](https://javascript.info/long-polling) techunique, client opens a connection that will hang until either change is detected, or timeout happens, if later, a new connection is oppened.
|
46
|
+
3. `:polling` - **[Deprecated]** This mode will use regular polling to detect changes, you can configure custom `polling_interval` (default is 100ms). We recommend using `:long_polling` as it makes much less requests to the server.
|
47
|
+
|
48
|
+
Keep in mind that `mode` configuration is **deprecated** and will be removed in the future, with `:websocket` be the only one available, in case you think that modes like `:long_polling` or `:polling` should be preserved feel free to open an issue.
|
46
49
|
|
47
50
|
### Create initializer `config/initializers/rails_live_reload.rb`:
|
48
51
|
|
@@ -50,8 +53,8 @@ $ bundle
|
|
50
53
|
```ruby
|
51
54
|
RailsLiveReload.configure do |config|
|
52
55
|
# config.url = "/rails/live/reload"
|
53
|
-
# Available modes are: :
|
54
|
-
# config.mode = :
|
56
|
+
# Available modes are: :websocket (default), :long_polling and :polling
|
57
|
+
# config.mode = :websocket
|
55
58
|
|
56
59
|
# This is used with :long_polling mode
|
57
60
|
# config.timeout = 30
|
@@ -78,7 +81,7 @@ There are 3 main parts:
|
|
78
81
|
|
79
82
|
1) listener of file changes (using `listen` gem)
|
80
83
|
2) collector of rendered views (see rails instrumentation)
|
81
|
-
3)
|
84
|
+
3) JavaScript client that communicates with server and triggers reloading when needed
|
82
85
|
|
83
86
|
## Notes
|
84
87
|
|
@@ -91,15 +94,12 @@ You are welcome to contribute. See list of `TODO's` below.
|
|
91
94
|
## TODO
|
92
95
|
|
93
96
|
- reload CSS without reloading the whole page?
|
94
|
-
- add `:websocket` mode?
|
95
97
|
- smarter reload if there is a change in helper (check methods from rendered views?)
|
96
98
|
- generator for initializer
|
97
99
|
- more complex rules? e.g. if "user.rb" file is changed - reload all pages with rendered "users" views
|
98
100
|
- check with older Rails versions
|
99
101
|
- tests or specs
|
100
102
|
- CI (github actions)
|
101
|
-
- improve how JS code is injected into HTML
|
102
|
-
- improve work with turbo, turbolinks
|
103
103
|
|
104
104
|
## License
|
105
105
|
|
@@ -18,11 +18,11 @@ module RailsLiveReload
|
|
18
18
|
end
|
19
19
|
|
20
20
|
class Config
|
21
|
-
attr_reader :patterns
|
22
|
-
attr_accessor :
|
21
|
+
attr_reader :patterns, :polling_interval, :timeout, :mode, :long_polling_sleep_duration
|
22
|
+
attr_accessor :url, :watcher, :files, :enabled
|
23
23
|
|
24
24
|
def initialize
|
25
|
-
@mode = :
|
25
|
+
@mode = :websocket
|
26
26
|
@timeout = 30
|
27
27
|
@long_polling_sleep_duration = 0.1
|
28
28
|
@polling_interval = 100
|
@@ -47,5 +47,25 @@ module RailsLiveReload
|
|
47
47
|
|
48
48
|
patterns[pattern] = reload
|
49
49
|
end
|
50
|
+
|
51
|
+
def mode=(mode)
|
52
|
+
warn "[DEPRECATION] RailsLiveReload 'mode' configuration is deprecated and will be removed in future versions #{caller.first}"
|
53
|
+
@mode = mode
|
54
|
+
end
|
55
|
+
|
56
|
+
def polling_interval=(polling_interval)
|
57
|
+
warn "[DEPRECATION] RailsLiveReload 'polling_interval' configuration is deprecated and will be removed in future versions #{caller.first}"
|
58
|
+
@polling_interval = polling_interval
|
59
|
+
end
|
60
|
+
|
61
|
+
def timeout=(timeout)
|
62
|
+
warn "[DEPRECATION] RailsLiveReload 'timeout' configuration is deprecated and will be removed in future versions #{caller.first}"
|
63
|
+
@timeout = timeout
|
64
|
+
end
|
65
|
+
|
66
|
+
def long_polling_sleep_duration=(long_polling_sleep_duration)
|
67
|
+
warn "[DEPRECATION] RailsLiveReload 'long_polling_sleep_duration' configuration is deprecated and will be removed in future versions #{caller.first}"
|
68
|
+
@long_polling_sleep_duration = long_polling_sleep_duration
|
69
|
+
end
|
50
70
|
end
|
51
71
|
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
/*!
|
2
|
+
Rails Live Reload 0.2.0
|
3
|
+
Copyright © 2022 RailsJazz
|
4
|
+
https://railsjazz.com
|
5
|
+
*/
|
6
|
+
var RailsLiveReload=function(){"use strict";const t="RELOAD";class e{static _instance;static get instance(){return e._instance||(e._instance=new this),e._instance}static start(){this.instance.start()}constructor(){this.initialize(),document.addEventListener("turbo:render",(()=>{document.documentElement.hasAttribute("data-turbo-preview")||this.restart()})),document.addEventListener("turbolinks:render",(()=>{document.documentElement.hasAttribute("data-turbolinks-preview")||this.restart()}))}initialize(){const{files:t,time:e,url:s,options:i}=JSON.parse(this.optionsNode.textContent);this.files=t,this.time=e,this.url=s,this.options=i}start(){throw"This should be implemented in subclass"}stop(){throw"This should be implemented in subclass"}restart(){this.stop(),this.initialize(),this.start()}fullReload(){window.location.reload()}get optionsNode(){const t=document.getElementById("rails-live-reload-options");if(!t)throw"Unable to find RailsLiveReload options";return t}}class s extends e{start(){this.retriesCount=0,this.timestamp=new Date,this.poll(this.timestamp)}stop(){this.timestamp=void 0}async poll(e){if(this.timestamp===e)try{const s=new FormData;s.append("dt",this.time),s.append("files",JSON.stringify(this.files));const i=await fetch(this.url,{method:"post",headers:{Accept:"application/json"},body:s}),n=await i.json();if(this.timestamp!==e)return;this.retriesCount=0,n.command===t?this.fullReload():this.poll(e)}catch(t){if(this.timestamp!==e)return;this.retriesCount++,this.retriesCount<10?setTimeout((()=>this.poll(e)),5e3):this.stop()}}}return document.addEventListener("DOMContentLoaded",(()=>{s.start()})),s}();
|
@@ -0,0 +1,6 @@
|
|
1
|
+
/*!
|
2
|
+
Rails Live Reload 0.2.0
|
3
|
+
Copyright © 2022 RailsJazz
|
4
|
+
https://railsjazz.com
|
5
|
+
*/
|
6
|
+
var RailsLiveReload=function(){"use strict";const t="RELOAD";class e{static _instance;static get instance(){return e._instance||(e._instance=new this),e._instance}static start(){this.instance.start()}constructor(){this.initialize(),document.addEventListener("turbo:render",(()=>{document.documentElement.hasAttribute("data-turbo-preview")||this.restart()})),document.addEventListener("turbolinks:render",(()=>{document.documentElement.hasAttribute("data-turbolinks-preview")||this.restart()}))}initialize(){const{files:t,time:e,url:i,options:s}=JSON.parse(this.optionsNode.textContent);this.files=t,this.time=e,this.url=i,this.options=s}start(){throw"This should be implemented in subclass"}stop(){throw"This should be implemented in subclass"}restart(){this.stop(),this.initialize(),this.start()}fullReload(){window.location.reload()}get optionsNode(){const t=document.getElementById("rails-live-reload-options");if(!t)throw"Unable to find RailsLiveReload options";return t}}class i extends e{start(){this.interval||(this.interval=setInterval((async()=>{const e=new FormData;e.append("dt",this.time),e.append("files",JSON.stringify(this.files));const i=await fetch(this.url,{method:"post",headers:{Accept:"application/json"},body:e});(await i.json()).command===t&&(this.stop(),this.fullReload())}),this.options.polling_interval))}restart(){this.initialize()}stop(){clearInterval(this.interval),this.interval=void 0}}return document.addEventListener("DOMContentLoaded",(()=>{i.start()})),i}();
|
@@ -0,0 +1,6 @@
|
|
1
|
+
/*!
|
2
|
+
Rails Live Reload 0.2.0
|
3
|
+
Copyright © 2022 RailsJazz
|
4
|
+
https://railsjazz.com
|
5
|
+
*/
|
6
|
+
var RailsLiveReload=function(){"use strict";const t="RELOAD";class e{static _instance;static get instance(){return e._instance||(e._instance=new this),e._instance}static start(){this.instance.start()}constructor(){this.initialize(),document.addEventListener("turbo:render",(()=>{document.documentElement.hasAttribute("data-turbo-preview")||this.restart()})),document.addEventListener("turbolinks:render",(()=>{document.documentElement.hasAttribute("data-turbolinks-preview")||this.restart()}))}initialize(){const{files:t,time:e,url:n,options:i}=JSON.parse(this.optionsNode.textContent);this.files=t,this.time=e,this.url=n,this.options=i}start(){throw"This should be implemented in subclass"}stop(){throw"This should be implemented in subclass"}restart(){this.stop(),this.initialize(),this.start()}fullReload(){window.location.reload()}get optionsNode(){const t=document.getElementById("rails-live-reload-options");if(!t)throw"Unable to find RailsLiveReload options";return t}}const n=["rails-live-reload-v1-json"];class i extends e{start(){this.connection||(this.connection=new WebSocket(function(t){if("function"==typeof t&&(t=t()),t&&!/^wss?:/i.test(t)){const e=document.createElement("a");return e.href=t,e.href=e.href,e.protocol=e.protocol.replace("http","ws"),e.href}return t}(this.url),n),this.connection.onmessage=this.handleMessage.bind(this),this.connection.onopen=this.handleConnectionOpen.bind(this),this.connection.onclose=this.handleConnectionClosed.bind(this))}stop(){this.connection.close()}restart(){this.initialize(),this.setupConnection()}setupConnection(){this.connection.send(JSON.stringify({event:"setup",options:{files:this.files,dt:this.time}}))}handleConnectionOpen(t){this.retriesCount=0,this.setupConnection()}handleMessage(e){JSON.parse(e.data).command===t&&this.fullReload()}handleConnectionClosed(t){this.connection=void 0,!t.wasClean&&this.retriesCount<=10&&(this.retriesCount++,setTimeout((()=>{this.start()}),1e3*this.retriesCount))}}return document.addEventListener("DOMContentLoaded",(()=>{i.start()})),i}();
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module RailsLiveReload
|
2
|
+
module Middleware
|
3
|
+
class Base
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
dup.call!(env)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call!(env)
|
13
|
+
request = Rack::Request.new(env)
|
14
|
+
|
15
|
+
case env["REQUEST_PATH"]
|
16
|
+
when RailsLiveReload.config.url
|
17
|
+
main_rails_live_response(request)
|
18
|
+
when "#{RailsLiveReload.config.url}/script"
|
19
|
+
content = client_javascript
|
20
|
+
[200, {'Content-Type' => 'application/javascript', 'Content-Length' => content.size.to_s, 'Cache-Control' => 'no-store'}, [content]]
|
21
|
+
else
|
22
|
+
status, headers, response = @app.call(env)
|
23
|
+
|
24
|
+
if html?(headers) && response.respond_to?(:[]) && (status == 500 || (status.to_s =~ /20./ && request.get?))
|
25
|
+
new_response = make_new_response(response[0])
|
26
|
+
headers['Content-Length'] = new_response.bytesize.to_s
|
27
|
+
response = [new_response]
|
28
|
+
end
|
29
|
+
|
30
|
+
[status, headers, response]
|
31
|
+
end
|
32
|
+
rescue Exception => ex
|
33
|
+
puts ex.message
|
34
|
+
puts ex.backtrace.take(10)
|
35
|
+
raise ex
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def main_rails_live_response(request)
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
|
44
|
+
def client_javascript
|
45
|
+
@client_javascript ||= File.open(File.join(File.dirname(__FILE__), "../javascript/#{RailsLiveReload.config.mode}.js")).read
|
46
|
+
end
|
47
|
+
|
48
|
+
def make_new_response(body)
|
49
|
+
body = body.sub("</head>", <<~HTML.html_safe)
|
50
|
+
<script defer type="text/javascript" src="#{RailsLiveReload.config.url}/script"></script>
|
51
|
+
</head>
|
52
|
+
HTML
|
53
|
+
body.sub("</body>", <<~HTML.html_safe)
|
54
|
+
<script id="rails-live-reload-options" type="application/json">
|
55
|
+
#{{
|
56
|
+
files: CurrentRequest.current.data.to_a,
|
57
|
+
time: Time.now.to_i,
|
58
|
+
url: RailsLiveReload.config.url,
|
59
|
+
options: javascript_options
|
60
|
+
}.to_json}
|
61
|
+
</script>
|
62
|
+
</body>
|
63
|
+
HTML
|
64
|
+
end
|
65
|
+
|
66
|
+
def javascript_options
|
67
|
+
{}
|
68
|
+
end
|
69
|
+
|
70
|
+
def html?(headers)
|
71
|
+
headers["Content-Type"].to_s.include?("text/html")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module RailsLiveReload
|
2
|
+
module Middleware
|
3
|
+
class LongPolling < Base
|
4
|
+
private
|
5
|
+
|
6
|
+
def main_rails_live_response(request)
|
7
|
+
params = request.params
|
8
|
+
body = lambda do |stream|
|
9
|
+
new_thread do
|
10
|
+
counter = 0
|
11
|
+
|
12
|
+
loop do
|
13
|
+
command = RailsLiveReload::Command.new(params)
|
14
|
+
|
15
|
+
if command.reload?
|
16
|
+
stream.write(command.payload.to_json) and break
|
17
|
+
end
|
18
|
+
|
19
|
+
sleep(RailsLiveReload.config.long_polling_sleep_duration)
|
20
|
+
counter += 1
|
21
|
+
|
22
|
+
stream.write(command.payload.to_json) and break if counter >= max_sleeps_count
|
23
|
+
end
|
24
|
+
ensure
|
25
|
+
stream.close
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
[ 200, { 'Content-Type' => 'application/json', 'rack.hijack' => body }, nil ]
|
30
|
+
end
|
31
|
+
|
32
|
+
def max_sleeps_count
|
33
|
+
RailsLiveReload.config.timeout * (1 / RailsLiveReload.config.long_polling_sleep_duration)
|
34
|
+
end
|
35
|
+
|
36
|
+
def new_thread
|
37
|
+
Thread.new {
|
38
|
+
t2 = Thread.current
|
39
|
+
t2.abort_on_exception = true
|
40
|
+
yield
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module RailsLiveReload
|
2
|
+
module Middleware
|
3
|
+
class Polling < Base
|
4
|
+
private
|
5
|
+
|
6
|
+
def main_rails_live_response(request)
|
7
|
+
[
|
8
|
+
200,
|
9
|
+
{ 'Content-Type' => 'application/json' },
|
10
|
+
[ RailsLiveReload::Command.new(request.params).payload.to_json ]
|
11
|
+
]
|
12
|
+
end
|
13
|
+
|
14
|
+
def javascript_options
|
15
|
+
{ polling_interval: RailsLiveReload.config.polling_interval }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rails_live_reload/web_socket/event_loop'
|
2
|
+
require 'rails_live_reload/web_socket/message_buffer'
|
3
|
+
require 'rails_live_reload/web_socket/wrapper'
|
4
|
+
require 'rails_live_reload/web_socket/client_socket'
|
5
|
+
require 'rails_live_reload/web_socket/stream'
|
6
|
+
require 'rails_live_reload/web_socket/base'
|
7
|
+
|
8
|
+
module RailsLiveReload
|
9
|
+
module Middleware
|
10
|
+
class WebSocket < Base
|
11
|
+
attr_reader :mutex, :event_loop
|
12
|
+
|
13
|
+
delegate :connections, :add_connection, :remove_connection, to: :class
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def connections
|
17
|
+
@connections ||= []
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_connection(connection)
|
21
|
+
connections << connection
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove_connection(connection)
|
25
|
+
connections.delete connection
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(env)
|
30
|
+
@mutex = Monitor.new
|
31
|
+
@event_loop = nil
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def main_rails_live_response(request)
|
36
|
+
setup_heartbeat_timer
|
37
|
+
RailsLiveReload::WebSocket::Base.new(self, request).process
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_heartbeat_timer
|
41
|
+
@heartbeat_timer ||= event_loop.timer(3) do
|
42
|
+
event_loop.post { connections.each(&:beat) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def event_loop
|
47
|
+
@event_loop || @mutex.synchronize { @event_loop ||= RailsLiveReload::WebSocket::EventLoop.new }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module RailsLiveReload
|
2
|
+
module WebSocket
|
3
|
+
# This class is strongly based on ActionCable
|
4
|
+
# https://github.com/rails/rails/blob/v7.0.3/actioncable/lib/action_cable/connection/base.rb
|
5
|
+
class Base
|
6
|
+
attr_reader :server, :env, :protocol, :request
|
7
|
+
attr_reader :dt, :files
|
8
|
+
|
9
|
+
delegate :event_loop, to: :server
|
10
|
+
|
11
|
+
def initialize(server, request)
|
12
|
+
@server, @request = server, request
|
13
|
+
@env = request.env
|
14
|
+
@websocket = RailsLiveReload::WebSocket::Wrapper.new(env, self, event_loop)
|
15
|
+
@message_buffer = RailsLiveReload::WebSocket::MessageBuffer.new(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def process
|
19
|
+
if websocket.possible?
|
20
|
+
respond_to_successful_request
|
21
|
+
else
|
22
|
+
respond_to_invalid_request
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def receive(websocket_message)
|
27
|
+
if websocket.alive?
|
28
|
+
handle_channel_command decode(websocket_message)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle_channel_command(payload)
|
33
|
+
case payload['event']
|
34
|
+
when 'setup'
|
35
|
+
setup payload['options']
|
36
|
+
else
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def reload
|
42
|
+
return if dt.nil? || files.nil? || RailsLiveReload::Checker.scan(dt, files).size.zero?
|
43
|
+
|
44
|
+
transmit({command: "RELOAD"})
|
45
|
+
end
|
46
|
+
|
47
|
+
def transmit(cable_message)
|
48
|
+
websocket.transmit encode(cable_message)
|
49
|
+
end
|
50
|
+
|
51
|
+
def close(reason: nil)
|
52
|
+
transmit({
|
53
|
+
type: RailsLiveReload::INTERNAL[:message_types][:disconnect],
|
54
|
+
reason: reason
|
55
|
+
})
|
56
|
+
websocket.close
|
57
|
+
end
|
58
|
+
|
59
|
+
def beat
|
60
|
+
transmit type: RailsLiveReload::INTERNAL[:message_types][:ping], message: Time.now.to_i
|
61
|
+
end
|
62
|
+
|
63
|
+
def on_open
|
64
|
+
@protocol = websocket.protocol
|
65
|
+
send_welcome_message
|
66
|
+
|
67
|
+
message_buffer.process!
|
68
|
+
server.add_connection(self)
|
69
|
+
end
|
70
|
+
|
71
|
+
def on_message(message)
|
72
|
+
message_buffer.append message
|
73
|
+
end
|
74
|
+
|
75
|
+
def on_error(message)
|
76
|
+
raise message
|
77
|
+
end
|
78
|
+
|
79
|
+
def on_close(reason, code)
|
80
|
+
server.remove_connection(self)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
attr_reader :websocket
|
86
|
+
attr_reader :message_buffer
|
87
|
+
|
88
|
+
def setup(options)
|
89
|
+
@dt = options['dt']
|
90
|
+
@files = options['files']
|
91
|
+
end
|
92
|
+
|
93
|
+
def encode(message)
|
94
|
+
message.to_json
|
95
|
+
end
|
96
|
+
|
97
|
+
def decode(message)
|
98
|
+
JSON.parse message
|
99
|
+
end
|
100
|
+
|
101
|
+
def send_welcome_message
|
102
|
+
transmit type: RailsLiveReload::INTERNAL[:message_types][:welcome]
|
103
|
+
end
|
104
|
+
|
105
|
+
def respond_to_successful_request
|
106
|
+
websocket.rack_response
|
107
|
+
end
|
108
|
+
|
109
|
+
def respond_to_invalid_request
|
110
|
+
close(reason: RailsLiveReload::INTERNAL[:disconnect_reasons][:invalid_request]) if websocket.alive?
|
111
|
+
|
112
|
+
[ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require "websocket/driver"
|
2
|
+
|
3
|
+
module RailsLiveReload
|
4
|
+
module WebSocket
|
5
|
+
# This class is basically copied from ActionCable
|
6
|
+
# https://github.com/rails/rails/blob/v7.0.3/actioncable/lib/action_cable/connection/client_socket.rb
|
7
|
+
class ClientSocket
|
8
|
+
def self.determine_url(env)
|
9
|
+
scheme = secure_request?(env) ? "wss:" : "ws:"
|
10
|
+
"#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.secure_request?(env)
|
14
|
+
return true if env["HTTPS"] == "on"
|
15
|
+
return true if env["HTTP_X_FORWARDED_SSL"] == "on"
|
16
|
+
return true if env["HTTP_X_FORWARDED_SCHEME"] == "https"
|
17
|
+
return true if env["HTTP_X_FORWARDED_PROTO"] == "https"
|
18
|
+
return true if env["rack.url_scheme"] == "https"
|
19
|
+
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
CONNECTING = 0
|
24
|
+
OPEN = 1
|
25
|
+
CLOSING = 2
|
26
|
+
CLOSED = 3
|
27
|
+
|
28
|
+
attr_reader :env, :url
|
29
|
+
|
30
|
+
def initialize(env, event_target, event_loop, protocols)
|
31
|
+
@env = env
|
32
|
+
@event_target = event_target
|
33
|
+
@event_loop = event_loop
|
34
|
+
|
35
|
+
@url = ClientSocket.determine_url(@env)
|
36
|
+
|
37
|
+
@driver = @driver_started = nil
|
38
|
+
@close_params = ["", 1006]
|
39
|
+
|
40
|
+
@ready_state = CONNECTING
|
41
|
+
|
42
|
+
@driver = ::WebSocket::Driver.rack(self, protocols: protocols)
|
43
|
+
|
44
|
+
@driver.on(:open) { |e| open }
|
45
|
+
@driver.on(:message) { |e| receive_message(e.data) }
|
46
|
+
@driver.on(:close) { |e| begin_close(e.reason, e.code) }
|
47
|
+
@driver.on(:error) { |e| emit_error(e.message) }
|
48
|
+
|
49
|
+
@stream = RailsLiveReload::WebSocket::Stream.new(@event_loop, self)
|
50
|
+
end
|
51
|
+
|
52
|
+
def start_driver
|
53
|
+
return if @driver.nil? || @driver_started
|
54
|
+
@stream.hijack_rack_socket
|
55
|
+
|
56
|
+
if callback = @env["async.callback"]
|
57
|
+
callback.call([101, {}, @stream])
|
58
|
+
end
|
59
|
+
|
60
|
+
@driver_started = true
|
61
|
+
@driver.start
|
62
|
+
end
|
63
|
+
|
64
|
+
def rack_response
|
65
|
+
start_driver
|
66
|
+
[ -1, {}, [] ]
|
67
|
+
end
|
68
|
+
|
69
|
+
def write(data)
|
70
|
+
@stream.write(data)
|
71
|
+
rescue => e
|
72
|
+
emit_error e.message
|
73
|
+
end
|
74
|
+
|
75
|
+
def transmit(message)
|
76
|
+
return false if @ready_state > OPEN
|
77
|
+
case message
|
78
|
+
when Numeric then @driver.text(message.to_s)
|
79
|
+
when String then @driver.text(message)
|
80
|
+
when Array then @driver.binary(message)
|
81
|
+
else false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def close(code = nil, reason = nil)
|
86
|
+
code ||= 1000
|
87
|
+
reason ||= ""
|
88
|
+
|
89
|
+
unless code == 1000 || (code >= 3000 && code <= 4999)
|
90
|
+
raise ArgumentError, "Failed to execute 'close' on WebSocket: " \
|
91
|
+
"The code must be either 1000, or between 3000 and 4999. " \
|
92
|
+
"#{code} is neither."
|
93
|
+
end
|
94
|
+
|
95
|
+
@ready_state = CLOSING unless @ready_state == CLOSED
|
96
|
+
@driver.close(reason, code)
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse(data)
|
100
|
+
@driver.parse(data)
|
101
|
+
end
|
102
|
+
|
103
|
+
def client_gone
|
104
|
+
finalize_close
|
105
|
+
end
|
106
|
+
|
107
|
+
def alive?
|
108
|
+
@ready_state == OPEN
|
109
|
+
end
|
110
|
+
|
111
|
+
def protocol
|
112
|
+
@driver.protocol
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def open
|
118
|
+
return unless @ready_state == CONNECTING
|
119
|
+
@ready_state = OPEN
|
120
|
+
|
121
|
+
@event_target.on_open
|
122
|
+
end
|
123
|
+
|
124
|
+
def receive_message(data)
|
125
|
+
return unless @ready_state == OPEN
|
126
|
+
|
127
|
+
@event_target.on_message(data)
|
128
|
+
end
|
129
|
+
|
130
|
+
def emit_error(message)
|
131
|
+
return if @ready_state >= CLOSING
|
132
|
+
|
133
|
+
@event_target.on_error(message)
|
134
|
+
end
|
135
|
+
|
136
|
+
def begin_close(reason, code)
|
137
|
+
return if @ready_state == CLOSED
|
138
|
+
@ready_state = CLOSING
|
139
|
+
@close_params = [reason, code]
|
140
|
+
|
141
|
+
@stream.shutdown if @stream
|
142
|
+
finalize_close
|
143
|
+
end
|
144
|
+
|
145
|
+
def finalize_close
|
146
|
+
return if @ready_state == CLOSED
|
147
|
+
@ready_state = CLOSED
|
148
|
+
|
149
|
+
@event_target.on_close(*@close_params)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require "nio"
|
2
|
+
|
3
|
+
module RailsLiveReload
|
4
|
+
module WebSocket
|
5
|
+
# This class is basically copied from ActionCable
|
6
|
+
# https://github.com/rails/rails/blob/v7.0.3/actioncable/lib/action_cable/connection/stream_event_loop.rb
|
7
|
+
class EventLoop
|
8
|
+
def initialize
|
9
|
+
@nio = @executor = @thread = nil
|
10
|
+
@map = {}
|
11
|
+
@stopping = false
|
12
|
+
@todo = Queue.new
|
13
|
+
@spawn_mutex = Mutex.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def timer(interval, &block)
|
17
|
+
Concurrent::TimerTask.new(execution_interval: interval, &block).tap(&:execute)
|
18
|
+
end
|
19
|
+
|
20
|
+
def post(task = nil, &block)
|
21
|
+
task ||= block
|
22
|
+
|
23
|
+
spawn
|
24
|
+
@executor << task
|
25
|
+
end
|
26
|
+
|
27
|
+
def attach(io, stream)
|
28
|
+
@todo << lambda do
|
29
|
+
@map[io] = @nio.register(io, :r)
|
30
|
+
@map[io].value = stream
|
31
|
+
end
|
32
|
+
wakeup
|
33
|
+
end
|
34
|
+
|
35
|
+
def detach(io, stream)
|
36
|
+
@todo << lambda do
|
37
|
+
@nio.deregister io
|
38
|
+
@map.delete io
|
39
|
+
io.close
|
40
|
+
end
|
41
|
+
wakeup
|
42
|
+
end
|
43
|
+
|
44
|
+
def writes_pending(io)
|
45
|
+
@todo << lambda do
|
46
|
+
if monitor = @map[io]
|
47
|
+
monitor.interests = :rw
|
48
|
+
end
|
49
|
+
end
|
50
|
+
wakeup
|
51
|
+
end
|
52
|
+
|
53
|
+
def stop
|
54
|
+
@stopping = true
|
55
|
+
wakeup if @nio
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def spawn
|
61
|
+
return if @thread && @thread.status
|
62
|
+
|
63
|
+
@spawn_mutex.synchronize do
|
64
|
+
return if @thread && @thread.status
|
65
|
+
|
66
|
+
@nio ||= NIO::Selector.new
|
67
|
+
|
68
|
+
@executor ||= Concurrent::ThreadPoolExecutor.new(
|
69
|
+
min_threads: 1,
|
70
|
+
max_threads: 10,
|
71
|
+
max_queue: 0,
|
72
|
+
)
|
73
|
+
|
74
|
+
@thread = Thread.new { run }
|
75
|
+
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def wakeup
|
81
|
+
spawn || @nio.wakeup
|
82
|
+
end
|
83
|
+
|
84
|
+
def run
|
85
|
+
loop do
|
86
|
+
if @stopping
|
87
|
+
@nio.close
|
88
|
+
break
|
89
|
+
end
|
90
|
+
|
91
|
+
until @todo.empty?
|
92
|
+
@todo.pop(true).call
|
93
|
+
end
|
94
|
+
|
95
|
+
next unless monitors = @nio.select
|
96
|
+
|
97
|
+
monitors.each do |monitor|
|
98
|
+
io = monitor.io
|
99
|
+
stream = monitor.value
|
100
|
+
|
101
|
+
begin
|
102
|
+
if monitor.writable?
|
103
|
+
if stream.flush_write_buffer
|
104
|
+
monitor.interests = :r
|
105
|
+
end
|
106
|
+
next unless monitor.readable?
|
107
|
+
end
|
108
|
+
|
109
|
+
incoming = io.read_nonblock(4096, exception: false)
|
110
|
+
case incoming
|
111
|
+
when :wait_readable
|
112
|
+
next
|
113
|
+
when nil
|
114
|
+
stream.close
|
115
|
+
else
|
116
|
+
stream.receive incoming
|
117
|
+
end
|
118
|
+
rescue
|
119
|
+
begin
|
120
|
+
stream.close
|
121
|
+
rescue
|
122
|
+
@nio.deregister io
|
123
|
+
@map.delete io
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module RailsLiveReload
|
2
|
+
module WebSocket
|
3
|
+
# This class is basically copied from ActionCable
|
4
|
+
# https://github.com/rails/rails/blob/v7.0.3/actioncable/lib/action_cable/connection/message_buffer.rb
|
5
|
+
class MessageBuffer
|
6
|
+
def initialize(connection)
|
7
|
+
@connection = connection
|
8
|
+
@buffered_messages = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def append(message)
|
12
|
+
if valid? message
|
13
|
+
if processing?
|
14
|
+
receive message
|
15
|
+
else
|
16
|
+
buffer message
|
17
|
+
end
|
18
|
+
else
|
19
|
+
raise ArgumentError, "Couldn't handle non-string message: #{message.class}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def processing?
|
24
|
+
@processing
|
25
|
+
end
|
26
|
+
|
27
|
+
def process!
|
28
|
+
@processing = true
|
29
|
+
receive_buffered_messages
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :connection, :buffered_messages
|
35
|
+
|
36
|
+
def valid?(message)
|
37
|
+
message.is_a?(String)
|
38
|
+
end
|
39
|
+
|
40
|
+
def receive(message)
|
41
|
+
connection.receive message
|
42
|
+
end
|
43
|
+
|
44
|
+
def buffer(message)
|
45
|
+
buffered_messages << message
|
46
|
+
end
|
47
|
+
|
48
|
+
def receive_buffered_messages
|
49
|
+
receive buffered_messages.shift until buffered_messages.empty?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module RailsLiveReload
|
2
|
+
module WebSocket
|
3
|
+
# This class is basically copied from ActionCable
|
4
|
+
# https://github.com/rails/rails/blob/v7.0.3/actioncable/lib/action_cable/connection/stream.rb
|
5
|
+
class Stream
|
6
|
+
def initialize(event_loop, socket)
|
7
|
+
@event_loop = event_loop
|
8
|
+
@socket_object = socket
|
9
|
+
@stream_send = socket.env["stream.send"]
|
10
|
+
|
11
|
+
@rack_hijack_io = nil
|
12
|
+
@write_lock = Mutex.new
|
13
|
+
|
14
|
+
@write_head = nil
|
15
|
+
@write_buffer = Queue.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def each(&callback)
|
19
|
+
@stream_send ||= callback
|
20
|
+
end
|
21
|
+
|
22
|
+
def close
|
23
|
+
shutdown
|
24
|
+
@socket_object.client_gone
|
25
|
+
end
|
26
|
+
|
27
|
+
def shutdown
|
28
|
+
clean_rack_hijack
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(data)
|
32
|
+
if @stream_send
|
33
|
+
return @stream_send.call(data)
|
34
|
+
end
|
35
|
+
|
36
|
+
if @write_lock.try_lock
|
37
|
+
begin
|
38
|
+
if @write_head.nil? && @write_buffer.empty?
|
39
|
+
written = @rack_hijack_io.write_nonblock(data, exception: false)
|
40
|
+
|
41
|
+
case written
|
42
|
+
when :wait_writable
|
43
|
+
when data.bytesize
|
44
|
+
return data.bytesize
|
45
|
+
else
|
46
|
+
@write_head = data.byteslice(written, data.bytesize)
|
47
|
+
@event_loop.writes_pending @rack_hijack_io
|
48
|
+
|
49
|
+
return data.bytesize
|
50
|
+
end
|
51
|
+
end
|
52
|
+
ensure
|
53
|
+
@write_lock.unlock
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@write_buffer << data
|
58
|
+
@event_loop.writes_pending @rack_hijack_io
|
59
|
+
|
60
|
+
data.bytesize
|
61
|
+
rescue EOFError, Errno::ECONNRESET
|
62
|
+
@socket_object.client_gone
|
63
|
+
end
|
64
|
+
|
65
|
+
def flush_write_buffer
|
66
|
+
@write_lock.synchronize do
|
67
|
+
loop do
|
68
|
+
if @write_head.nil?
|
69
|
+
return true if @write_buffer.empty?
|
70
|
+
@write_head = @write_buffer.pop
|
71
|
+
end
|
72
|
+
|
73
|
+
written = @rack_hijack_io.write_nonblock(@write_head, exception: false)
|
74
|
+
case written
|
75
|
+
when :wait_writable
|
76
|
+
return false
|
77
|
+
when @write_head.bytesize
|
78
|
+
@write_head = nil
|
79
|
+
else
|
80
|
+
@write_head = @write_head.byteslice(written, @write_head.bytesize)
|
81
|
+
return false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def receive(data)
|
88
|
+
@socket_object.parse(data)
|
89
|
+
end
|
90
|
+
|
91
|
+
def hijack_rack_socket
|
92
|
+
return unless @socket_object.env["rack.hijack"]
|
93
|
+
|
94
|
+
@rack_hijack_io = @socket_object.env["rack.hijack"].call
|
95
|
+
@rack_hijack_io ||= @socket_object.env["rack.hijack_io"]
|
96
|
+
|
97
|
+
@event_loop.attach(@rack_hijack_io, self)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def clean_rack_hijack
|
103
|
+
return unless @rack_hijack_io
|
104
|
+
@event_loop.detach(@rack_hijack_io, self)
|
105
|
+
@rack_hijack_io = nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "websocket/driver"
|
2
|
+
|
3
|
+
module RailsLiveReload
|
4
|
+
module WebSocket
|
5
|
+
# This class is basically copied from ActionCable
|
6
|
+
# https://github.com/rails/rails/blob/v7.0.3/actioncable/lib/action_cable/connection/web_socket.rb
|
7
|
+
class Wrapper
|
8
|
+
delegate :transmit, :close, :protocol, :rack_response, to: :websocket
|
9
|
+
|
10
|
+
def initialize(env, event_target, event_loop, protocols: RailsLiveReload::INTERNAL[:protocols])
|
11
|
+
@websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, event_loop, protocols) : nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def possible?
|
15
|
+
websocket
|
16
|
+
end
|
17
|
+
|
18
|
+
def alive?
|
19
|
+
websocket && websocket.alive?
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :websocket
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/rails_live_reload.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
require "listen"
|
2
2
|
require "rails_live_reload/version"
|
3
3
|
require "rails_live_reload/config"
|
4
|
-
require "rails_live_reload/client"
|
5
4
|
require "rails_live_reload/watcher"
|
6
|
-
require "rails_live_reload/
|
7
|
-
require "rails_live_reload/
|
8
|
-
require "rails_live_reload/
|
5
|
+
require "rails_live_reload/middleware/base"
|
6
|
+
require "rails_live_reload/middleware/long_polling"
|
7
|
+
require "rails_live_reload/middleware/web_socket"
|
8
|
+
require "rails_live_reload/middleware/polling"
|
9
9
|
require "rails_live_reload/instrument/metrics_collector"
|
10
10
|
require "rails_live_reload/thread/current_request"
|
11
11
|
require "rails_live_reload/checker"
|
@@ -16,10 +16,24 @@ module RailsLiveReload
|
|
16
16
|
mattr_accessor :watcher
|
17
17
|
@@watcher = {}
|
18
18
|
|
19
|
+
INTERNAL = {
|
20
|
+
message_types: {
|
21
|
+
welcome: "welcome",
|
22
|
+
disconnect: "disconnect",
|
23
|
+
ping: "ping",
|
24
|
+
},
|
25
|
+
disconnect_reasons: {
|
26
|
+
invalid_request: "invalid_request",
|
27
|
+
remote: "remote"
|
28
|
+
},
|
29
|
+
protocols: ["rails-live-reload-v1-json"].freeze
|
30
|
+
}
|
31
|
+
|
19
32
|
def self.middleware
|
20
33
|
case config.mode
|
21
|
-
when :polling then RailsLiveReload::
|
22
|
-
when :long_polling then RailsLiveReload::
|
34
|
+
when :polling then RailsLiveReload::Middleware::Polling
|
35
|
+
when :long_polling then RailsLiveReload::Middleware::LongPolling
|
36
|
+
when :websocket then RailsLiveReload::Middleware::WebSocket
|
23
37
|
end
|
24
38
|
end
|
25
39
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_live_reload
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Igor Kasyanchuk
|
@@ -9,10 +9,10 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-07-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
15
|
+
name: railties
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
18
|
- - ">="
|
@@ -40,41 +40,13 @@ dependencies:
|
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '0'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
|
-
name:
|
43
|
+
name: websocket-driver
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
46
|
- - ">="
|
47
47
|
- !ruby/object:Gem::Version
|
48
48
|
version: '0'
|
49
|
-
type: :
|
50
|
-
prerelease: false
|
51
|
-
version_requirements: !ruby/object:Gem::Requirement
|
52
|
-
requirements:
|
53
|
-
- - ">="
|
54
|
-
- !ruby/object:Gem::Version
|
55
|
-
version: '0'
|
56
|
-
- !ruby/object:Gem::Dependency
|
57
|
-
name: pry
|
58
|
-
requirement: !ruby/object:Gem::Requirement
|
59
|
-
requirements:
|
60
|
-
- - ">="
|
61
|
-
- !ruby/object:Gem::Version
|
62
|
-
version: '0'
|
63
|
-
type: :development
|
64
|
-
prerelease: false
|
65
|
-
version_requirements: !ruby/object:Gem::Requirement
|
66
|
-
requirements:
|
67
|
-
- - ">="
|
68
|
-
- !ruby/object:Gem::Version
|
69
|
-
version: '0'
|
70
|
-
- !ruby/object:Gem::Dependency
|
71
|
-
name: pry-nav
|
72
|
-
requirement: !ruby/object:Gem::Requirement
|
73
|
-
requirements:
|
74
|
-
- - ">="
|
75
|
-
- !ruby/object:Gem::Version
|
76
|
-
version: '0'
|
77
|
-
type: :development
|
49
|
+
type: :runtime
|
78
50
|
prerelease: false
|
79
51
|
version_requirements: !ruby/object:Gem::Requirement
|
80
52
|
requirements:
|
@@ -82,13 +54,13 @@ dependencies:
|
|
82
54
|
- !ruby/object:Gem::Version
|
83
55
|
version: '0'
|
84
56
|
- !ruby/object:Gem::Dependency
|
85
|
-
name:
|
57
|
+
name: nio4r
|
86
58
|
requirement: !ruby/object:Gem::Requirement
|
87
59
|
requirements:
|
88
60
|
- - ">="
|
89
61
|
- !ruby/object:Gem::Version
|
90
62
|
version: '0'
|
91
|
-
type: :
|
63
|
+
type: :runtime
|
92
64
|
prerelease: false
|
93
65
|
version_requirements: !ruby/object:Gem::Requirement
|
94
66
|
requirements:
|
@@ -123,17 +95,26 @@ files:
|
|
123
95
|
- Rakefile
|
124
96
|
- lib/rails_live_reload.rb
|
125
97
|
- lib/rails_live_reload/checker.rb
|
126
|
-
- lib/rails_live_reload/client.rb
|
127
98
|
- lib/rails_live_reload/command.rb
|
128
99
|
- lib/rails_live_reload/config.rb
|
129
100
|
- lib/rails_live_reload/engine.rb
|
130
101
|
- lib/rails_live_reload/instrument/metrics_collector.rb
|
131
|
-
- lib/rails_live_reload/
|
132
|
-
- lib/rails_live_reload/
|
133
|
-
- lib/rails_live_reload/
|
102
|
+
- lib/rails_live_reload/javascript/long_polling.js
|
103
|
+
- lib/rails_live_reload/javascript/polling.js
|
104
|
+
- lib/rails_live_reload/javascript/websocket.js
|
105
|
+
- lib/rails_live_reload/middleware/base.rb
|
106
|
+
- lib/rails_live_reload/middleware/long_polling.rb
|
107
|
+
- lib/rails_live_reload/middleware/polling.rb
|
108
|
+
- lib/rails_live_reload/middleware/web_socket.rb
|
134
109
|
- lib/rails_live_reload/thread/current_request.rb
|
135
110
|
- lib/rails_live_reload/version.rb
|
136
111
|
- lib/rails_live_reload/watcher.rb
|
112
|
+
- lib/rails_live_reload/web_socket/base.rb
|
113
|
+
- lib/rails_live_reload/web_socket/client_socket.rb
|
114
|
+
- lib/rails_live_reload/web_socket/event_loop.rb
|
115
|
+
- lib/rails_live_reload/web_socket/message_buffer.rb
|
116
|
+
- lib/rails_live_reload/web_socket/stream.rb
|
117
|
+
- lib/rails_live_reload/web_socket/wrapper.rb
|
137
118
|
homepage: https://github.com/railsjazz/rails_live_reload
|
138
119
|
licenses:
|
139
120
|
- MIT
|
@@ -1,75 +0,0 @@
|
|
1
|
-
module RailsLiveReload
|
2
|
-
module Client
|
3
|
-
|
4
|
-
def Client.long_polling_js
|
5
|
-
<<~HTML.html_safe
|
6
|
-
<script>
|
7
|
-
(function() {
|
8
|
-
const files = #{CurrentRequest.current.data.to_a.to_json};
|
9
|
-
let retries_count = 0;
|
10
|
-
function poll() {
|
11
|
-
const formData = new FormData();
|
12
|
-
formData.append('dt', #{Time.now.to_i})
|
13
|
-
formData.append('files', JSON.stringify(files))
|
14
|
-
|
15
|
-
fetch(
|
16
|
-
"#{RailsLiveReload.config.url}",
|
17
|
-
{
|
18
|
-
method: "post",
|
19
|
-
headers: { 'Accept': 'application/json', },
|
20
|
-
body: formData
|
21
|
-
}
|
22
|
-
)
|
23
|
-
.then(response => response.json())
|
24
|
-
.then(data => {
|
25
|
-
retries_count = 0;
|
26
|
-
if(data['command'] === 'RELOAD') {
|
27
|
-
window.location.reload();
|
28
|
-
} else {
|
29
|
-
poll();
|
30
|
-
}
|
31
|
-
}).catch(() => {
|
32
|
-
retries_count++;
|
33
|
-
|
34
|
-
if(retries_count < 10) {
|
35
|
-
setTimeout(poll, 5000)
|
36
|
-
}
|
37
|
-
})
|
38
|
-
}
|
39
|
-
poll();
|
40
|
-
})();
|
41
|
-
</script>
|
42
|
-
HTML
|
43
|
-
end
|
44
|
-
|
45
|
-
def Client.polling_js
|
46
|
-
<<~HTML.html_safe
|
47
|
-
<script>
|
48
|
-
const files = #{CurrentRequest.current.data.to_a.to_json};
|
49
|
-
const timer = setInterval(
|
50
|
-
() => {
|
51
|
-
const formData = new FormData();
|
52
|
-
formData.append('dt', #{Time.now.to_i})
|
53
|
-
formData.append('files', JSON.stringify(files))
|
54
|
-
fetch(
|
55
|
-
"#{RailsLiveReload.config.url}",
|
56
|
-
{
|
57
|
-
method: "post",
|
58
|
-
headers: { 'Accept': 'application/json', },
|
59
|
-
body: formData
|
60
|
-
}
|
61
|
-
)
|
62
|
-
.then(response => response.json())
|
63
|
-
.then(data => {
|
64
|
-
if(data['command'] === 'RELOAD') {
|
65
|
-
clearInterval(timer);
|
66
|
-
window.location.reload();
|
67
|
-
}
|
68
|
-
})
|
69
|
-
}, #{RailsLiveReload.config.polling_interval}
|
70
|
-
)
|
71
|
-
</script>
|
72
|
-
HTML
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
@@ -1,63 +0,0 @@
|
|
1
|
-
module RailsLiveReload
|
2
|
-
module Rails
|
3
|
-
module Middleware
|
4
|
-
class Base
|
5
|
-
def initialize(app)
|
6
|
-
@app = app
|
7
|
-
end
|
8
|
-
|
9
|
-
def call(env)
|
10
|
-
dup.call!(env)
|
11
|
-
end
|
12
|
-
|
13
|
-
def call!(env)
|
14
|
-
request = Rack::Request.new(env)
|
15
|
-
|
16
|
-
if env["REQUEST_PATH"] == RailsLiveReload.config.url
|
17
|
-
rails_live_response(request)
|
18
|
-
else
|
19
|
-
@status, @headers, @response = @app.call(env)
|
20
|
-
|
21
|
-
if html? && @response.respond_to?(:[]) && (@status == 500 || (@status.to_s =~ /20./ && request.get?))
|
22
|
-
new_response = make_new_response(@response[0])
|
23
|
-
@headers['Content-Length'] = new_response.bytesize.to_s
|
24
|
-
@response = [new_response]
|
25
|
-
end
|
26
|
-
|
27
|
-
[@status, @headers, @response]
|
28
|
-
end
|
29
|
-
rescue Exception => ex
|
30
|
-
puts ex.message
|
31
|
-
puts ex.backtrace.take(10)
|
32
|
-
raise ex
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
def rails_live_response(request)
|
38
|
-
raise NotImplementedError
|
39
|
-
end
|
40
|
-
|
41
|
-
def client_javascript(request)
|
42
|
-
raise NotImplementedError
|
43
|
-
end
|
44
|
-
|
45
|
-
def make_new_response(body)
|
46
|
-
body.sub("</head>", "</head>#{client_javascript}")
|
47
|
-
end
|
48
|
-
|
49
|
-
def html?
|
50
|
-
@headers["Content-Type"].to_s.include?("text/html")
|
51
|
-
end
|
52
|
-
|
53
|
-
def new_thread
|
54
|
-
Thread.new {
|
55
|
-
t2 = Thread.current
|
56
|
-
t2.abort_on_exception = true
|
57
|
-
yield
|
58
|
-
}
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
@@ -1,51 +0,0 @@
|
|
1
|
-
module RailsLiveReload
|
2
|
-
module Rails
|
3
|
-
module Middleware
|
4
|
-
class LongPolling < Base
|
5
|
-
private
|
6
|
-
|
7
|
-
def rails_live_response(request)
|
8
|
-
params = request.params
|
9
|
-
body = lambda do |stream|
|
10
|
-
new_thread do
|
11
|
-
counter = 0
|
12
|
-
|
13
|
-
loop do
|
14
|
-
command = RailsLiveReload::Command.new(params)
|
15
|
-
|
16
|
-
if command.reload?
|
17
|
-
stream.write(command.payload.to_json) and break
|
18
|
-
end
|
19
|
-
|
20
|
-
sleep(RailsLiveReload.config.long_polling_sleep_duration)
|
21
|
-
counter += 1
|
22
|
-
|
23
|
-
stream.write(command.payload.to_json) and break if counter >= max_sleeps_count
|
24
|
-
end
|
25
|
-
ensure
|
26
|
-
stream.close
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
[ 200, { 'Content-Type' => 'application/json', 'rack.hijack' => body }, nil ]
|
31
|
-
end
|
32
|
-
|
33
|
-
def client_javascript
|
34
|
-
RailsLiveReload::Client.long_polling_js
|
35
|
-
end
|
36
|
-
|
37
|
-
def max_sleeps_count
|
38
|
-
RailsLiveReload.config.timeout * (1 / RailsLiveReload.config.long_polling_sleep_duration)
|
39
|
-
end
|
40
|
-
|
41
|
-
def new_thread
|
42
|
-
Thread.new {
|
43
|
-
t2 = Thread.current
|
44
|
-
t2.abort_on_exception = true
|
45
|
-
yield
|
46
|
-
}
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module RailsLiveReload
|
2
|
-
module Rails
|
3
|
-
module Middleware
|
4
|
-
class Polling < Base
|
5
|
-
private
|
6
|
-
|
7
|
-
def rails_live_response(request)
|
8
|
-
[
|
9
|
-
200,
|
10
|
-
{ 'Content-Type' => 'application/json' },
|
11
|
-
[ RailsLiveReload::Command.new(request.params).payload.to_json ]
|
12
|
-
]
|
13
|
-
end
|
14
|
-
|
15
|
-
def client_javascript
|
16
|
-
RailsLiveReload::Client.polling_js
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|