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.
@@ -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