rails_live_reload 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 453d155a64c2869fea44301e05aa33cf32274ff501cfdb6a7eb1c061bb5cffa1
4
- data.tar.gz: b7e734f2649d12365e8e346e2656fd386aea36187d6a295f19f8643460138d6d
3
+ metadata.gz: 999e453ba10fc303a5f317609ad52abbb318972712802ea87606f17c89c2054a
4
+ data.tar.gz: 456e9df00f34d48e75a8cdbee655e035e68a0c98c468020946dc3ec6903848bb
5
5
  SHA512:
6
- metadata.gz: 7519b3b746a4c58eb412e97743a24db2b39ae7e99351bff81edcfe69df682478e0c250f5b9f5a08022fd1c086f571df0bc52cfaa530bf0c1996c1d1240456ea7
7
- data.tar.gz: 347cae5497f229eb42a38cd9e62f3fc8c33b1c2f03542af5546a156a54f80fff7e5fa4a16e3253e69a44b6e5df9ebe25e5930fd03e19a9d941d2be7a43fb431b
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. No 3rd party dependencies.
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 two modes:
44
- 1. `:long_polling` - This is a default mode, it 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
45
- 2. `:polling` - 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.
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: :long_polling (default) and :polling
54
- # config.mode = :long_polling
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) middleware which is responding to polling JS calls
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 :mode, :polling_interval, :timeout, :url, :watcher, :files, :enabled
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 = :long_polling
25
+ @mode = :websocket
26
26
  @timeout = 30
27
- @patterns = {}
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? && !defined?(Rails::Console)
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
@@ -1,3 +1,3 @@
1
1
  module RailsLiveReload
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -27,6 +27,7 @@ module RailsLiveReload
27
27
  all.each do |file|
28
28
  files[file] = File.mtime(file).to_i rescue nil
29
29
  end
30
+ RailsLiveReload::Middleware::WebSocket.connections.each(&:reload)
30
31
  end
31
32
  listener.start
32
33
  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