rails_live_reload 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,11 +1,10 @@
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/rails/middleware/base"
7
- require "rails_live_reload/rails/middleware/long_polling"
8
- require "rails_live_reload/rails/middleware/polling"
5
+ require "rails_live_reload/server/connections"
6
+ require "rails_live_reload/server/base"
7
+ require "rails_live_reload/middleware/base"
9
8
  require "rails_live_reload/instrument/metrics_collector"
10
9
  require "rails_live_reload/thread/current_request"
11
10
  require "rails_live_reload/checker"
@@ -16,10 +15,23 @@ module RailsLiveReload
16
15
  mattr_accessor :watcher
17
16
  @@watcher = {}
18
17
 
19
- def self.middleware
20
- case config.mode
21
- when :polling then RailsLiveReload::Rails::Middleware::Polling
22
- when :long_polling then RailsLiveReload::Rails::Middleware::LongPolling
23
- end
18
+ INTERNAL = {
19
+ message_types: {
20
+ welcome: "welcome",
21
+ disconnect: "disconnect",
22
+ ping: "ping",
23
+ },
24
+ disconnect_reasons: {
25
+ invalid_request: "invalid_request",
26
+ remote: "remote"
27
+ },
28
+ socket_events: {
29
+ reload: 'reload'
30
+ },
31
+ protocols: ["rails-live-reload-v1-json"].freeze
32
+ }
33
+
34
+ module_function def server
35
+ @server ||= RailsLiveReload::Server::Base.new
24
36
  end
25
37
  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.1.1
4
+ version: 0.3.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-05-31 00:00:00.000000000 Z
12
+ date: 2022-07-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: rails
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: puma
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: :development
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: sprockets-rails
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: :development
63
+ type: :runtime
92
64
  prerelease: false
93
65
  version_requirements: !ruby/object:Gem::Requirement
94
66
  requirements:
@@ -123,17 +95,23 @@ 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/rails/middleware/base.rb
132
- - lib/rails_live_reload/rails/middleware/long_polling.rb
133
- - lib/rails_live_reload/rails/middleware/polling.rb
102
+ - lib/rails_live_reload/javascript/websocket.js
103
+ - lib/rails_live_reload/middleware/base.rb
104
+ - lib/rails_live_reload/server/base.rb
105
+ - lib/rails_live_reload/server/connections.rb
134
106
  - lib/rails_live_reload/thread/current_request.rb
135
107
  - lib/rails_live_reload/version.rb
136
108
  - lib/rails_live_reload/watcher.rb
109
+ - lib/rails_live_reload/web_socket/base.rb
110
+ - lib/rails_live_reload/web_socket/client_socket.rb
111
+ - lib/rails_live_reload/web_socket/event_loop.rb
112
+ - lib/rails_live_reload/web_socket/message_buffer.rb
113
+ - lib/rails_live_reload/web_socket/stream.rb
114
+ - lib/rails_live_reload/web_socket/wrapper.rb
137
115
  homepage: https://github.com/railsjazz/rails_live_reload
138
116
  licenses:
139
117
  - 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