rails_live_reload 0.1.0 → 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 +16 -10
- data/lib/rails_live_reload/config.rb +37 -5
- data/lib/rails_live_reload/engine.rb +1 -1
- 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 -12
- metadata +21 -40
- 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 -53
- 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,12 @@ $ 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
|
58
|
+
|
59
|
+
# This is used with :long_polling mode
|
60
|
+
# config.timeout = 30
|
61
|
+
# config.long_polling_sleep_duration = 0.1
|
55
62
|
|
56
63
|
# This is used with :polling mode
|
57
64
|
# config.polling_interval = 100
|
@@ -63,6 +70,8 @@ RailsLiveReload.configure do |config|
|
|
63
70
|
# More examples:
|
64
71
|
# config.watch %r{app/helpers/.+\.rb}, reload: :always
|
65
72
|
# config.watch %r{config/locales/.+\.yml}, reload: :always
|
73
|
+
|
74
|
+
# config.enabled = Rails.env.development?
|
66
75
|
end if defined?(RailsLiveReload)
|
67
76
|
```
|
68
77
|
|
@@ -72,7 +81,7 @@ There are 3 main parts:
|
|
72
81
|
|
73
82
|
1) listener of file changes (using `listen` gem)
|
74
83
|
2) collector of rendered views (see rails instrumentation)
|
75
|
-
3)
|
84
|
+
3) JavaScript client that communicates with server and triggers reloading when needed
|
76
85
|
|
77
86
|
## Notes
|
78
87
|
|
@@ -85,15 +94,12 @@ You are welcome to contribute. See list of `TODO's` below.
|
|
85
94
|
## TODO
|
86
95
|
|
87
96
|
- reload CSS without reloading the whole page?
|
88
|
-
- add `:websocket` mode?
|
89
97
|
- smarter reload if there is a change in helper (check methods from rendered views?)
|
90
98
|
- generator for initializer
|
91
99
|
- more complex rules? e.g. if "user.rb" file is changed - reload all pages with rendered "users" views
|
92
100
|
- check with older Rails versions
|
93
101
|
- tests or specs
|
94
102
|
- CI (github actions)
|
95
|
-
- improve how JS code is injected into HTML
|
96
|
-
- improve work with turbo, turbolinks
|
97
103
|
|
98
104
|
## License
|
99
105
|
|
@@ -18,22 +18,54 @@ 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
|
29
29
|
@url = "/rails/live/reload"
|
30
30
|
@watcher = nil
|
31
31
|
@files = {}
|
32
|
-
@enabled = ::Rails.env.development?
|
32
|
+
@enabled = ::Rails.env.development?
|
33
|
+
|
34
|
+
# These configs work for 95% apps, see README for more info
|
35
|
+
@patterns = {
|
36
|
+
%r{app/views/.+\.(erb|haml|slim)$} => :on_change,
|
37
|
+
%r{(app|vendor)/(assets|javascript)/\w+/(.+\.(css|js|html|png|jpg|ts|jsx)).*} => :always
|
38
|
+
}
|
39
|
+
@default_patterns_changed = false
|
33
40
|
end
|
34
41
|
|
35
42
|
def watch(pattern, reload: :on_change)
|
43
|
+
unless @default_patterns_changed
|
44
|
+
@default_patterns_changed = true
|
45
|
+
@patterns = {}
|
46
|
+
end
|
47
|
+
|
36
48
|
patterns[pattern] = reload
|
37
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
|
38
70
|
end
|
39
71
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module RailsLiveReload
|
2
2
|
class Railtie < ::Rails::Engine
|
3
3
|
|
4
|
-
if RailsLiveReload.enabled?
|
4
|
+
if RailsLiveReload.enabled? && defined?(::Rails::Server)
|
5
5
|
initializer "rails_live_reload.middleware" do |app|
|
6
6
|
if ::Rails::VERSION::MAJOR.to_i >= 5
|
7
7
|
app.middleware.insert_after ActionDispatch::Executor, RailsLiveReload.middleware
|
@@ -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
|