anycable-rails 1.0.0.preview2 → 1.0.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.
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