rails_live_reload 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|