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.
@@ -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,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/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/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,16 +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::Rails::Middleware::Polling
22
- when :long_polling then RailsLiveReload::Rails::Middleware::LongPolling
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
26
-
27
- # These configs work for 95% apps, see README for more info
28
- RailsLiveReload.configure do |config|
29
- config.watch %r{app/views/.+\.(erb|haml|slim)$}
30
- config.watch %r{(app|vendor)/(assets|javascript)/\w+/(.+\.(css|js|html|png|jpg|ts|jsx)).*}, reload: :always
31
- 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.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-05-31 00:00:00.000000000 Z
12
+ date: 2022-07-09 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,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/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/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
@@ -153,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
134
  - !ruby/object:Gem::Version
154
135
  version: '0'
155
136
  requirements: []
156
- rubygems_version: 3.2.3
137
+ rubygems_version: 3.3.7
157
138
  signing_key:
158
139
  specification_version: 4
159
140
  summary: Ruby on Rails Live Reload
@@ -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,53 +0,0 @@
1
- module RailsLiveReload
2
- module Rails
3
- module Middleware
4
- class LongPolling < Base
5
- private
6
-
7
- POLLING_SLEEP = 0.1
8
-
9
- def rails_live_response(request)
10
- params = request.params
11
- body = lambda do |stream|
12
- new_thread do
13
- counter = 0
14
-
15
- loop do
16
- command = RailsLiveReload::Command.new(params)
17
-
18
- if command.reload?
19
- stream.write(command.payload.to_json) and break
20
- end
21
-
22
- sleep(POLLING_SLEEP)
23
- counter += 1
24
-
25
- stream.write(command.payload.to_json) and break if counter >= max_sleeps_count
26
- end
27
- ensure
28
- stream.close
29
- end
30
- end
31
-
32
- [ 200, { 'Content-Type' => 'application/json', 'rack.hijack' => body }, nil ]
33
- end
34
-
35
- def client_javascript
36
- RailsLiveReload::Client.long_polling_js
37
- end
38
-
39
- def max_sleeps_count
40
- RailsLiveReload.config.timeout * (1 / POLLING_SLEEP)
41
- end
42
-
43
- def new_thread
44
- Thread.new {
45
- t2 = Thread.current
46
- t2.abort_on_exception = true
47
- yield
48
- }
49
- end
50
- end
51
- end
52
- end
53
- 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