anycable-rails 1.0.0.preview2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -122
  3. data/README.md +14 -34
  4. data/lib/anycable/rails.rb +36 -2
  5. data/lib/anycable/rails/actioncable/channel.rb +4 -0
  6. data/lib/anycable/rails/actioncable/connection.rb +39 -35
  7. data/lib/anycable/rails/actioncable/connection/persistent_session.rb +7 -3
  8. data/lib/anycable/rails/actioncable/connection/serializable_identification.rb +42 -0
  9. data/lib/anycable/rails/actioncable/remote_connections.rb +11 -0
  10. data/lib/anycable/rails/actioncable/testing.rb +35 -0
  11. data/lib/anycable/rails/channel_state.rb +46 -0
  12. data/lib/anycable/rails/compatibility.rb +4 -7
  13. data/lib/anycable/rails/compatibility/rubocop.rb +0 -1
  14. data/lib/anycable/rails/compatibility/rubocop/config/default.yml +3 -0
  15. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/instance_vars.rb +1 -1
  16. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb +4 -4
  17. data/lib/anycable/rails/railtie.rb +26 -18
  18. data/lib/anycable/rails/refinements/subscriptions.rb +5 -0
  19. data/lib/anycable/rails/session_proxy.rb +19 -2
  20. data/lib/anycable/rails/version.rb +1 -1
  21. data/lib/generators/anycable/download/USAGE +14 -0
  22. data/lib/generators/anycable/download/download_generator.rb +83 -0
  23. data/lib/generators/anycable/setup/USAGE +2 -0
  24. data/lib/{rails/generators → generators}/anycable/setup/setup_generator.rb +100 -88
  25. data/lib/generators/anycable/setup/templates/Procfile.dev.tt +6 -0
  26. data/lib/{rails/generators → generators}/anycable/setup/templates/config/anycable.yml.tt +22 -4
  27. data/lib/generators/anycable/setup/templates/config/cable.yml.tt +11 -0
  28. data/lib/{rails/generators/anycable/setup/templates/config/initializers/anycable.rb → generators/anycable/setup/templates/config/initializers/anycable.rb.tt} +1 -1
  29. data/lib/generators/anycable/with_os_helpers.rb +55 -0
  30. metadata +29 -52
  31. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/remote_disconnect.rb +0 -31
  32. data/lib/rails/generators/anycable/setup/templates/Procfile +0 -2
  33. data/lib/rails/generators/anycable/setup/templates/Procfile.dev +0 -3
  34. data/lib/rails/generators/anycable/setup/templates/bin/heroku-web +0 -7
  35. data/lib/rails/generators/anycable/setup/templates/config/cable.yml.tt +0 -11
@@ -0,0 +1,2 @@
1
+ Description:
2
+ Configures your application to work with AnyCable interactively.
@@ -1,34 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "generators/anycable/with_os_helpers"
4
+
3
5
  module AnyCableRailsGenerators
4
6
  # Entry point for interactive installation
5
7
  class SetupGenerator < ::Rails::Generators::Base
6
8
  namespace "anycable:setup"
7
9
  source_root File.expand_path("templates", __dir__)
8
10
 
9
- METHODS = %w[skip local docker].freeze
10
- # TODO(release): change to latest release
11
- SERVER_VERSION = "v1.0.0.preview1"
12
- OS_NAMES = %w[linux darwin freebsd win].freeze
13
- CPU_NAMES = %w[amd64 arm64 386 arm].freeze
11
+ DOCS_ROOT = "https://docs.anycable.io/v1/#"
12
+ DEVELOPMENT_METHODS = %w[skip local docker].freeze
14
13
  SERVER_SOURCES = %w[skip brew binary].freeze
15
- DEFAULT_BIN_PATH = "/usr/local/bin"
16
14
 
17
- class_option :method,
15
+ class_option :devenv,
18
16
  type: :string,
19
- desc: "Select your development environment (options: #{METHODS.join(", ")})"
17
+ desc: "Select your development environment (options: #{DEVELOPMENT_METHODS.join(", ")})"
20
18
  class_option :source,
21
19
  type: :string,
22
20
  desc: "Choose a way of installing AnyCable-Go server (options: #{SERVER_SOURCES.join(", ")})"
23
- class_option :bin_path,
24
- type: :string,
25
- desc: "Where to download AnyCable-Go server binary (default: #{DEFAULT_BIN_PATH})"
26
- class_option :os,
27
- type: :string,
28
- desc: "Specify the OS for AnyCable-Go server binary (options: #{OS_NAMES.join(", ")})"
29
- class_option :cpu,
30
- type: :string,
31
- desc: "Specify the CPU architecturefor AnyCable-Go server binary (options: #{CPU_NAMES.join(", ")})"
32
21
  class_option :skip_heroku,
33
22
  type: :boolean,
34
23
  desc: "Do not copy Heroku configs"
@@ -36,6 +25,15 @@ module AnyCableRailsGenerators
36
25
  type: :boolean,
37
26
  desc: "Do not create Procfile.dev"
38
27
 
28
+ include WithOSHelpers
29
+
30
+ class_option :bin_path,
31
+ type: :string,
32
+ desc: "Where to download AnyCable-Go server binary (default: #{DEFAULT_BIN_PATH})"
33
+ class_option :version,
34
+ type: :string,
35
+ desc: "Specify the AnyCable-Go version (defaults to latest release)"
36
+
39
37
  def welcome
40
38
  say "👋 Welcome to AnyCable interactive installer."
41
39
  end
@@ -51,32 +49,36 @@ module AnyCableRailsGenerators
51
49
  environment(nil, env: :development) do
52
50
  <<~SNIPPET
53
51
  # Specify AnyCable WebSocket server URL to use by JS client
54
- config.action_cable.url = ENV.fetch("CABLE_URL", "ws://localhost:3334/cable").presence
52
+ config.after_initialize do
53
+ config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL", "ws://localhost:8080/cable") if AnyCable::Rails.enabled?
54
+ end
55
55
  SNIPPET
56
56
  end
57
57
 
58
58
  environment(nil, env: :production) do
59
59
  <<~SNIPPET
60
60
  # Specify AnyCable WebSocket server URL to use by JS client
61
- config.action_cable.url = ENV["CABLE_URL"].presence
61
+ config.after_initialize do
62
+ config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL") if AnyCable::Rails.enabled?
63
+ end
62
64
  SNIPPET
63
65
  end
64
66
 
65
67
  say_status :info, "✅ 'config.action_cable.url' has been configured"
66
- say_status :help, "⚠️ If you're using JS client make sure you have " \
68
+ say_status :help, "⚠️ If you're using JS client make sure you have " \
67
69
  "`action_cable_meta_tag` included before any <script> tag in your application.html"
68
70
  end
69
71
 
70
72
  def development_method
71
- answer = METHODS.index(options[:method]) || 99
73
+ answer = DEVELOPMENT_METHODS.index(options[:devenv]) || 99
72
74
 
73
- until METHODS[answer.to_i]
75
+ until DEVELOPMENT_METHODS[answer.to_i]
74
76
  answer = ask "Which environment do you use for development? (1) Local, (2) Docker, (0) Skip"
75
77
  end
76
78
 
77
- case env = METHODS[answer.to_i]
79
+ case env = DEVELOPMENT_METHODS[answer.to_i]
78
80
  when "skip"
79
- say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 https://docs.anycable.io/#/anycable-go/getting_started", :yellow
81
+ say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 #{DOCS_ROOT}/anycable-go/getting_started", :yellow
80
82
  else
81
83
  send "install_for_#{env}"
82
84
  end
@@ -84,15 +86,21 @@ module AnyCableRailsGenerators
84
86
 
85
87
  def heroku
86
88
  if options[:skip_heroku].nil?
87
- return unless yes? "Do you use Heroku for deployment?"
89
+ return unless yes? "Do you use Heroku for deployment? [Yn]"
88
90
  elsif options[:skip_heroku]
89
91
  return
90
92
  end
91
93
 
92
- template "Procfile"
93
- inside("bin") { template "heroku-web" }
94
+ in_root do
95
+ next unless File.file?("Procfile")
96
+
97
+ contents = File.read("Procfile")
98
+ contents.sub!(/^web: (.*)$/, %q(web: [[ "$ANYCABLE_DEPLOYMENT" == "true" ]] && bundle exec anycable --server-command="anycable-go" || \1))
99
+ File.write("Procfile", contents)
100
+ say_status :info, "✅ Procfile updated"
101
+ end
94
102
 
95
- say_status :help, "️️⚠️ Please, read the required steps to configure Heroku applications 👉 https://docs.anycable.io/#/deployment/heroku", :yellow
103
+ say_status :help, "️️⚠️ Please, read the required steps to configure Heroku applications 👉 #{DOCS_ROOT}/deployment/heroku", :yellow
96
104
  end
97
105
 
98
106
  def devise
@@ -107,6 +115,20 @@ module AnyCableRailsGenerators
107
115
  say_status :info, "✅ config/initializers/anycable.rb with Devise configuration has been added"
108
116
  end
109
117
 
118
+ def stimulus_reflex
119
+ return unless stimulus_reflex?
120
+
121
+ say_status :help, "⚠️ Please, check out the documentation on using AnyCable with Stimulus Reflex: https://docs.anycable.io/v1/#/ruby/stimulus_reflex"
122
+ end
123
+
124
+ def rubocop_compatibility
125
+ return unless rubocop?
126
+
127
+ say_status :info, "🤖 Running static compatibility checks with RuboCop"
128
+ res = run "bundle exec rubocop -r 'anycable/rails/compatibility/rubocop' --only AnyCable/InstanceVars,AnyCable/PeriodicalTimers,AnyCable/InstanceVars"
129
+ say_status :help, "⚠️ Please, take a look at the icompatibilities above and fix them. See https://docs.anycable.io/v1/#/ruby/compatibility" unless res
130
+ end
131
+
110
132
  def finish
111
133
  say_status :info, "✅ AnyCable has been configured successfully!"
112
134
  end
@@ -114,7 +136,19 @@ module AnyCableRailsGenerators
114
136
  private
115
137
 
116
138
  def stimulus_reflex?
117
- !gemfile_lock&.match?(/^\s+stimulus_reflex\b/).nil?
139
+ !!gemfile_lock&.match?(/^\s+stimulus_reflex\b/)
140
+ end
141
+
142
+ def redis?
143
+ !!gemfile_lock&.match?(/^\s+redis\b/)
144
+ end
145
+
146
+ def webpacker?
147
+ !!gemfile_lock&.match?(/^\s+webpacker\b/)
148
+ end
149
+
150
+ def rubocop?
151
+ !!gemfile_lock&.match?(/^\s+rubocop\b/)
118
152
  end
119
153
 
120
154
  def gemfile_lock
@@ -129,32 +163,41 @@ module AnyCableRailsGenerators
129
163
  end
130
164
 
131
165
  def install_for_docker
132
- say_status :help, "️️⚠️ Docker development configuration could vary", :yellow
166
+ # Remove localhost from configuraiton
167
+ gsub_file "config/anycable.yml", /^.*redis_url:.*localhost[^\n]+\n/, ""
168
+
169
+ say_status :help, "️️⚠️ Docker development configuration could vary", :yellow
133
170
 
134
171
  say "Here is an example snippet for docker-compose.yml:"
135
172
  say <<~YML
136
173
  ─────────────────────────────────────────
137
- anycable-ws:
138
- image: anycable/anycable-go:v0.6.4
174
+ ws:
175
+ image: anycable/anycable-go:1.0
139
176
  ports:
140
- - '3334:3334'
177
+ - '8080:8080'
141
178
  environment:
142
- PORT: 3334
179
+ ANYCABLE_HOST: "0.0.0.0"
143
180
  ANYCABLE_REDIS_URL: redis://redis:6379/0
144
- ANYCABLE_RPC_HOST: anycable-rpc:50051
181
+ ANYCABLE_RPC_HOST: anycable:50051
182
+ ANYCABLE_DEBUG: 1
145
183
  depends_on:
146
- - anycable-rpc
147
- - redis
184
+ redis:
185
+ condition: service_healthy
148
186
 
149
- anycable-rpc:
187
+ anycable:
150
188
  <<: *backend
151
189
  command: bundle exec anycable
152
190
  environment:
153
191
  <<: *backend_environment
154
192
  ANYCABLE_REDIS_URL: redis://redis:6379/0
155
193
  ANYCABLE_RPC_HOST: 0.0.0.0:50051
194
+ ANYCABLE_DEBUG: 1
156
195
  ports:
157
196
  - '50051'
197
+ depends_on:
198
+ <<: *backend_depends_on
199
+ ws:
200
+ condition: service_started
158
201
  ─────────────────────────────────────────
159
202
  YML
160
203
  end
@@ -173,7 +216,7 @@ module AnyCableRailsGenerators
173
216
 
174
217
  case answer.to_i
175
218
  when 0
176
- say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 https://docs.anycable.io/#/anycable-go/getting_started", :yellow
219
+ say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 #{DOCS_ROOT}/anycable-go/getting_started", :yellow
177
220
  return
178
221
  else
179
222
  return unless send("install_from_#{SERVER_SOURCES[answer.to_i]}")
@@ -185,8 +228,8 @@ module AnyCableRailsGenerators
185
228
  def template_proc_files
186
229
  file_name = "Procfile.dev"
187
230
 
188
- if File.exist?(file_name)
189
- append_file file_name, 'anycable: bundle exec anycable --server-command "anycable-go --port 3334"'
231
+ if file_exists?(file_name)
232
+ append_file file_name, "anycable: bundle exec anycable\nws: anycable-go#{anycable_go_options}", force: true
190
233
  else
191
234
  say_status :help, "💡 We recommend using Hivemind to manage multiple processes in development 👉 https://github.com/DarthSim/hivemind", :yellow
192
235
 
@@ -206,62 +249,31 @@ module AnyCableRailsGenerators
206
249
  end
207
250
 
208
251
  def install_from_binary
209
- out = options[:bin_path] if options[:bin_path]
210
- out ||= "/usr/local/bin" if options[:method] # User don't want interactive mode
211
- out ||= ask "Please, enter the path to download the AnyCable-Go binary to", default: DEFAULT_BIN_PATH, path: true
212
-
213
- os_name = options[:os] ||
214
- OS_NAMES.find(&Gem::Platform.local.os.method(:==)) ||
215
- ask("What is your OS name?", limited_to: OS_NAMES)
216
-
217
- cpu_name = options[:cpu] ||
218
- CPU_NAMES.find(&current_cpu.method(:==)) ||
219
- ask("What is your CPU architecture?", limited_to: CPU_NAMES)
252
+ bin_path ||= DEFAULT_BIN_PATH if options[:devenv] # User don't want interactive mode
253
+ bin_path ||= ask "Please, enter the path to download the AnyCable-Go binary to", default: DEFAULT_BIN_PATH, path: true
220
254
 
221
- download_exe(
222
- "https://github.com/anycable/anycable-go/releases/download/#{SERVER_VERSION}/" \
223
- "anycable-go-#{os_name}-#{cpu_name}",
224
- to: out,
225
- file_name: "anycable-go"
226
- )
255
+ generate "anycable:download", download_options(bin_path: bin_path)
227
256
 
228
257
  true
229
258
  end
230
259
 
231
- def download_exe(url, to:, file_name:)
232
- file_path = File.join(to, file_name)
233
-
234
- run "#{sudo(to)}curl -L #{url} -o #{file_path}", abort_on_failure: true
235
- run "#{sudo(to)}chmod +x #{file_path}", abort_on_failure: true
236
- run "#{file_path} -v", abort_on_failure: true
260
+ def download_options(**params)
261
+ opts = options.merge(params)
262
+ [].tap do |args|
263
+ args << "--os #{opts[:os]}" if opts[:os]
264
+ args << "--cpu #{opts[:cpu]}" if opts[:cpu]
265
+ args << "--bin-path=#{opts[:bin_path]}" if opts[:bin_path]
266
+ args << "--version #{opts[:version]}" if opts[:version]
267
+ end.join(" ")
237
268
  end
238
269
 
239
- def sudo!(path)
240
- sudo = ""
241
- unless File.writable?(path)
242
- if yes? "Path is not writable 😕. Do you have sudo privileges?"
243
- sudo = "sudo "
244
- else
245
- say_status :error, "❌ Failed to install AnyCable-Go WebSocket server", :red
246
- raise StandardError, "Path #{path} is not writable!"
247
- end
248
- end
249
-
250
- sudo
270
+ def anycable_go_options
271
+ redis? ? " --port=8080" : " --port=8080 --broadcast_adapter=http"
251
272
  end
252
273
 
253
- def current_cpu
254
- case Gem::Platform.local.cpu
255
- when "x86_64", "x64"
256
- "amd64"
257
- when "x86_32", "x86", "i386", "i486", "i686"
258
- "i386"
259
- when "aarch64", "aarch64_be", /armv8/
260
- "arm64"
261
- when "arm", /armv7/, /armv6/
262
- "arm"
263
- else
264
- "unknown"
274
+ def file_exists?(name)
275
+ in_root do
276
+ return File.file?(name)
265
277
  end
266
278
  end
267
279
  end
@@ -0,0 +1,6 @@
1
+ server: bundle exec rails s
2
+ <%- if webpacker? -%>
3
+ assets: bundle exec bin/webpack-dev-server
4
+ <%- end -%>
5
+ anycable: bundle exec anycable
6
+ ws: anycable-go<%= anycable_go_options %>
@@ -7,26 +7,44 @@
7
7
  # Note that AnyCable recognizes REDIS_URL env variable for Redis pub/sub adapter. If you want to
8
8
  # use another Redis instance for AnyCable, provide ANYCABLE_REDIS_URL variable.
9
9
  #
10
- # Read more about AnyCable configuration here: https://docs.anycable.io/#/ruby/configuration
10
+ # Read more about AnyCable configuration here: <%= DOCS_ROOT %>/ruby/configuration
11
11
  #
12
12
  default: &default
13
13
  # Turn on/off access logs ("Started..." and "Finished...")
14
14
  access_logs_disabled: false
15
- # Persist "dirty" session between RPC calls (might be required for StimulusReflex apps)
16
- persistent_session_enabled: <%= stimulus_reflex? %>
15
+ # Persist "dirty" session between RPC calls (might be required for apps using stimulus_reflex <3.0)
16
+ # persistent_session_enabled: true
17
17
  # This is the host and the port to run AnyCable RPC server on.
18
18
  # You must configure your WebSocket server to connect to it, e.g.:
19
19
  # $ anycable-go --rpc-host="<rpc hostname>:50051"
20
20
  rpc_host: "127.0.0.1:50051"
21
21
  # Whether to enable gRPC level logging or not
22
22
  log_grpc: false
23
+ <%- if redis? -%>
24
+ # Use Redis to broadcast messages to AnyCable server
25
+ broadcast_adapter: redis
26
+ <%- else -%>
27
+ # Use HTTP adapter for a quick start (since redis gem is not present in the project)
28
+ broadcast_adapter: http
29
+ <%- end -%>
23
30
  # Use the same channel name for WebSocket server, e.g.:
24
31
  # $ anycable-go --redis-channel="__anycable__"
25
32
  redis_channel: "__anycable__"
33
+ <%- if redis? -%>
34
+ # You can use REDIS_URL env var to configure Redis URL.
35
+ # Localhost is used by default.
36
+ # redis_url: "redis://localhost:6379/1"
37
+ <%- end -%>
26
38
 
27
39
  development:
28
40
  <<: *default
29
- redis_url: "redis://localhost:6379/1"
41
+
42
+ test:
43
+ <<: *default
30
44
 
31
45
  production:
32
46
  <<: *default
47
+ <%- if !redis? -%>
48
+ # Use Redis in production
49
+ broadcast_adapter: redis
50
+ <%- end -%>
@@ -0,0 +1,11 @@
1
+ # Make it possible to switch adapters by passing the ACTION_CABLE_ADAPTER env variable.
2
+ # For example, you can use it fallback to the standard Action Cable in staging/review
3
+ # environments (by setting `ACTION_CABLE_ADAPTER=redis`).
4
+ development:
5
+ adapter: <%%= ENV.fetch("ACTION_CABLE_ADAPTER", "any_cable") %>
6
+
7
+ test:
8
+ adapter: test
9
+
10
+ production:
11
+ adapter: <%%= ENV.fetch("ACTION_CABLE_ADAPTER", "any_cable") %>
@@ -3,7 +3,7 @@
3
3
  # Add Warden middkeware to AnyCable stack to allow accessing
4
4
  # Devise current user via `env["warden"].user`.
5
5
  #
6
- # See http://docs.anycable.io/#/ruby/authentication
6
+ # See <%= DOCS_ROOT %>/ruby/authentication
7
7
  AnyCable::Rails::Rack.middleware.use Warden::Manager do |config|
8
8
  Devise.warden_config = config
9
9
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCableRailsGenerators
4
+ module WithOSHelpers
5
+ OS_NAMES = %w[linux darwin freebsd win].freeze
6
+ CPU_NAMES = %w[amd64 arm64 386 arm].freeze
7
+ DEFAULT_BIN_PATH = "/usr/local/bin"
8
+
9
+ def self.included(base)
10
+ base.class_option :os,
11
+ type: :string,
12
+ desc: "Specify the OS for AnyCable-Go server binary (options: #{OS_NAMES.join(", ")})"
13
+ base.class_option :cpu,
14
+ type: :string,
15
+ desc: "Specify the CPU architecturefor AnyCable-Go server binary (options: #{CPU_NAMES.join(", ")})"
16
+
17
+ private :current_cpu, :supported_current_cpu, :supported_current_os
18
+ end
19
+
20
+ def current_cpu
21
+ case Gem::Platform.local.cpu
22
+ when "x86_64", "x64"
23
+ "amd64"
24
+ when "x86_32", "x86", "i386", "i486", "i686"
25
+ "i386"
26
+ when "aarch64", "aarch64_be", /armv8/
27
+ "arm64"
28
+ when "arm", /armv7/, /armv6/
29
+ "arm"
30
+ else
31
+ "unknown"
32
+ end
33
+ end
34
+
35
+ def os_name
36
+ options[:os] ||
37
+ supported_current_os ||
38
+ ask("What is your OS name?", limited_to: OS_NAMES)
39
+ end
40
+
41
+ def cpu_name
42
+ options[:cpu] ||
43
+ supported_current_cpu ||
44
+ ask("What is your CPU architecture?", limited_to: CPU_NAMES)
45
+ end
46
+
47
+ def supported_current_cpu
48
+ CPU_NAMES.find(&current_cpu.method(:==))
49
+ end
50
+
51
+ def supported_current_os
52
+ OS_NAMES.find(&Gem::Platform.local.os.method(:==))
53
+ end
54
+ end
55
+ end