anycable-rails 0.6.5 → 1.0.0.rc3

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 -110
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +34 -37
  5. data/lib/action_cable/subscription_adapter/any_cable.rb +2 -1
  6. data/lib/anycable/rails.rb +37 -2
  7. data/lib/anycable/rails/actioncable/channel.rb +4 -0
  8. data/lib/anycable/rails/actioncable/connection.rb +72 -50
  9. data/lib/anycable/rails/actioncable/connection/persistent_session.rb +34 -0
  10. data/lib/anycable/rails/actioncable/connection/serializable_identification.rb +42 -0
  11. data/lib/anycable/rails/actioncable/remote_connections.rb +11 -0
  12. data/lib/anycable/rails/actioncable/testing.rb +35 -0
  13. data/lib/anycable/rails/channel_state.rb +46 -0
  14. data/lib/anycable/rails/compatibility.rb +7 -10
  15. data/lib/anycable/rails/compatibility/rubocop.rb +0 -1
  16. data/lib/anycable/rails/compatibility/rubocop/config/default.yml +3 -1
  17. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/instance_vars.rb +1 -1
  18. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb +4 -4
  19. data/lib/anycable/rails/config.rb +8 -4
  20. data/lib/anycable/rails/rack.rb +56 -0
  21. data/lib/anycable/rails/railtie.rb +28 -13
  22. data/lib/anycable/rails/refinements/subscriptions.rb +1 -1
  23. data/lib/anycable/rails/session_proxy.rb +79 -0
  24. data/lib/anycable/rails/version.rb +1 -1
  25. data/lib/generators/anycable/download/USAGE +14 -0
  26. data/lib/generators/anycable/download/download_generator.rb +83 -0
  27. data/lib/generators/anycable/setup/USAGE +2 -0
  28. data/lib/generators/anycable/setup/setup_generator.rb +266 -0
  29. data/lib/generators/anycable/setup/templates/Procfile.dev +3 -0
  30. data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +43 -0
  31. data/lib/generators/anycable/setup/templates/config/cable.yml.tt +11 -0
  32. data/lib/generators/anycable/setup/templates/config/initializers/anycable.rb.tt +9 -0
  33. data/lib/generators/anycable/with_os_helpers.rb +55 -0
  34. metadata +45 -30
  35. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/remote_disconnect.rb +0 -31
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AnyCable
4
4
  module Rails
5
- VERSION = "0.6.5"
5
+ VERSION = "1.0.0.rc3"
6
6
  end
7
7
  end
@@ -0,0 +1,14 @@
1
+ Description:
2
+ Install AnyCable-Go web server locally (the latest version by default).
3
+
4
+ Example:
5
+ rails generate anycable:download
6
+
7
+ This will ask:
8
+ Where to store a binary file.
9
+ This will create:
10
+ `<bin_path>/anycable-go`.
11
+
12
+ rails generate anycable:download --bin-path=/usr/local/bin
13
+
14
+ rails generate anycable:download --version=1.0.0
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "generators/anycable/with_os_helpers"
4
+
5
+ module AnyCableRailsGenerators
6
+ # Downloads anycable-go binary
7
+ class DownloadGenerator < ::Rails::Generators::Base
8
+ namespace "anycable:download"
9
+
10
+ include WithOSHelpers
11
+
12
+ VERSION = "latest"
13
+
14
+ class_option :bin_path,
15
+ type: :string,
16
+ desc: "Where to download AnyCable-Go server binary (default: #{DEFAULT_BIN_PATH})"
17
+ class_option :version,
18
+ type: :string,
19
+ desc: "Specify the AnyCable-Go version (defaults to latest release)"
20
+
21
+ def download_bin
22
+ out = options[:bin_path] || DEFAULT_BIN_PATH
23
+ version = options[:version] || VERSION
24
+
25
+ download_exe(
26
+ release_url(version),
27
+ to: out,
28
+ file_name: "anycable-go"
29
+ )
30
+
31
+ true
32
+ end
33
+
34
+ private
35
+
36
+ def release_url(version)
37
+ return latest_release_url(version) if version == "latest"
38
+
39
+ if Gem::Version.new(version).segments.first >= 1
40
+ new_release_url("v#{version}")
41
+ else
42
+ legacy_release_url("v#{version}")
43
+ end
44
+ end
45
+
46
+ def legacy_release_url(version)
47
+ "https://github.com/anycable/anycable-go/releases/download/#{version}/" \
48
+ "anycable-go-v#{version}-#{os_name}-#{cpu_name}"
49
+ end
50
+
51
+ def new_release_url(version)
52
+ "https://github.com/anycable/anycable-go/releases/download/#{version}/" \
53
+ "anycable-go-#{os_name}-#{cpu_name}"
54
+ end
55
+
56
+ def latest_release_url(version)
57
+ "https://github.com/anycable/anycable-go/releases/latest/download/" \
58
+ "anycable-go-#{os_name}-#{cpu_name}"
59
+ end
60
+
61
+ def download_exe(url, to:, file_name:)
62
+ file_path = File.join(to, file_name)
63
+
64
+ run "#{sudo(to)}curl -L #{url} -o #{file_path}", abort_on_failure: true
65
+ run "#{sudo(to)}chmod +x #{file_path}", abort_on_failure: true
66
+ run "#{file_path} -v", abort_on_failure: true
67
+ end
68
+
69
+ def sudo(path)
70
+ sudo = ""
71
+ unless File.writable?(path)
72
+ if yes? "Path is not writable 😕. Do you have sudo privileges?"
73
+ sudo = "sudo "
74
+ else
75
+ say_status :error, "❌ Failed to install AnyCable-Go WebSocket server", :red
76
+ raise StandardError, "Path #{path} is not writable!"
77
+ end
78
+ end
79
+
80
+ sudo
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,2 @@
1
+ Description:
2
+ Configures your application to work with AnyCable interactively.
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "generators/anycable/with_os_helpers"
4
+
5
+ module AnyCableRailsGenerators
6
+ # Entry point for interactive installation
7
+ class SetupGenerator < ::Rails::Generators::Base
8
+ namespace "anycable:setup"
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ DOCS_ROOT = "https://docs.anycable.io/v1/#"
12
+ DEVELOPMENT_METHODS = %w[skip local docker].freeze
13
+ SERVER_SOURCES = %w[skip brew binary].freeze
14
+
15
+ class_option :devenv,
16
+ type: :string,
17
+ desc: "Select your development environment (options: #{DEVELOPMENT_METHODS.join(", ")})"
18
+ class_option :source,
19
+ type: :string,
20
+ desc: "Choose a way of installing AnyCable-Go server (options: #{SERVER_SOURCES.join(", ")})"
21
+ class_option :skip_heroku,
22
+ type: :boolean,
23
+ desc: "Do not copy Heroku configs"
24
+ class_option :skip_procfile_dev,
25
+ type: :boolean,
26
+ desc: "Do not create Procfile.dev"
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
+
37
+ def welcome
38
+ say "👋 Welcome to AnyCable interactive installer."
39
+ end
40
+
41
+ def configs
42
+ inside("config") do
43
+ template "cable.yml"
44
+ template "anycable.yml"
45
+ end
46
+ end
47
+
48
+ def cable_url
49
+ environment(nil, env: :development) do
50
+ <<~SNIPPET
51
+ # Specify AnyCable WebSocket server URL to use by JS client
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
+ SNIPPET
56
+ end
57
+
58
+ environment(nil, env: :production) do
59
+ <<~SNIPPET
60
+ # Specify AnyCable WebSocket server URL to use by JS client
61
+ config.after_initialize do
62
+ config.action_cable.url = ActionCable.server.config.url = ENV.fetch("CABLE_URL") if AnyCable::Rails.enabled?
63
+ end
64
+ SNIPPET
65
+ end
66
+
67
+ say_status :info, "✅ 'config.action_cable.url' has been configured"
68
+ say_status :help, "⚠️ If you're using JS client make sure you have " \
69
+ "`action_cable_meta_tag` included before any <script> tag in your application.html"
70
+ end
71
+
72
+ def development_method
73
+ answer = DEVELOPMENT_METHODS.index(options[:devenv]) || 99
74
+
75
+ until DEVELOPMENT_METHODS[answer.to_i]
76
+ answer = ask "Which environment do you use for development? (1) Local, (2) Docker, (0) Skip"
77
+ end
78
+
79
+ case env = DEVELOPMENT_METHODS[answer.to_i]
80
+ when "skip"
81
+ say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 #{DOCS_ROOT}/anycable-go/getting_started", :yellow
82
+ else
83
+ send "install_for_#{env}"
84
+ end
85
+ end
86
+
87
+ def heroku
88
+ if options[:skip_heroku].nil?
89
+ return unless yes? "Do you use Heroku for deployment? [Yn]"
90
+ elsif options[:skip_heroku]
91
+ return
92
+ end
93
+
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
102
+
103
+ say_status :help, "️️⚠️ Please, read the required steps to configure Heroku applications 👉 #{DOCS_ROOT}/deployment/heroku", :yellow
104
+ end
105
+
106
+ def devise
107
+ in_root do
108
+ return unless File.file?("config/initializers/devise.rb")
109
+ end
110
+
111
+ inside("config/initializers") do
112
+ template "anycable.rb"
113
+ end
114
+
115
+ say_status :info, "✅ config/initializers/anycable.rb with Devise configuration has been added"
116
+ end
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
+
132
+ def finish
133
+ say_status :info, "✅ AnyCable has been configured successfully!"
134
+ end
135
+
136
+ private
137
+
138
+ def stimulus_reflex?
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 rubocop?
147
+ !!gemfile_lock&.match?(/^\s+rubocop\b/)
148
+ end
149
+
150
+ def gemfile_lock
151
+ @gemfile_lock ||= begin
152
+ res = nil
153
+ in_root do
154
+ next unless File.file?("Gemfile.lock")
155
+ res = File.read("Gemfile.lock")
156
+ end
157
+ res
158
+ end
159
+ end
160
+
161
+ def install_for_docker
162
+ # Remove localhost from configuraiton
163
+ gsub_file "config/anycable.yml", /^.*redis_url:.*localhost[^\n]+\n/, ""
164
+
165
+ say_status :help, "️️⚠️ Docker development configuration could vary", :yellow
166
+
167
+ say "Here is an example snippet for docker-compose.yml:"
168
+ say <<~YML
169
+ ─────────────────────────────────────────
170
+ ws:
171
+ image: anycable/anycable-go:1.0
172
+ ports:
173
+ - '8080:8080'
174
+ environment:
175
+ ANYCABLE_HOST: "0.0.0.0"
176
+ ANYCABLE_REDIS_URL: redis://redis:6379/0
177
+ ANYCABLE_RPC_HOST: anycable:50051
178
+ ANYCABLE_DEBUG: 1
179
+ depends_on:
180
+ redis:
181
+ condition: service_healthy
182
+
183
+ anycable:
184
+ <<: *backend
185
+ command: bundle exec anycable
186
+ environment:
187
+ <<: *backend_environment
188
+ ANYCABLE_REDIS_URL: redis://redis:6379/0
189
+ ANYCABLE_RPC_HOST: 0.0.0.0:50051
190
+ ANYCABLE_DEBUG: 1
191
+ ports:
192
+ - '50051'
193
+ depends_on:
194
+ <<: *backend_depends_on
195
+ ws:
196
+ condition: service_started
197
+ ─────────────────────────────────────────
198
+ YML
199
+ end
200
+
201
+ def install_for_local
202
+ install_server
203
+ template_proc_files
204
+ end
205
+
206
+ def install_server
207
+ answer = SERVER_SOURCES.index(options[:source]) || 99
208
+
209
+ until SERVER_SOURCES[answer.to_i]
210
+ answer = ask "How do you want to install AnyCable-Go WebSocket server? (1) Homebrew, (2) Download binary, (0) Skip"
211
+ end
212
+
213
+ case answer.to_i
214
+ when 0
215
+ say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 #{DOCS_ROOT}/anycable-go/getting_started", :yellow
216
+ return
217
+ else
218
+ return unless send("install_from_#{SERVER_SOURCES[answer.to_i]}")
219
+ end
220
+
221
+ say_status :info, "✅ AnyCable-Go WebSocket server has been successfully installed"
222
+ end
223
+
224
+ def template_proc_files
225
+ file_name = "Procfile.dev"
226
+
227
+ if File.exist?(file_name)
228
+ append_file file_name, 'anycable: bundle exec anycable --server-command "anycable-go --port 3334"'
229
+ else
230
+ say_status :help, "💡 We recommend using Hivemind to manage multiple processes in development 👉 https://github.com/DarthSim/hivemind", :yellow
231
+
232
+ if options[:skip_procfile_dev].nil?
233
+ return unless yes? "Do you want to create a '#{file_name}' file?"
234
+ elsif options[:skip_procfile_dev]
235
+ return
236
+ end
237
+
238
+ template file_name
239
+ end
240
+ end
241
+
242
+ def install_from_brew
243
+ run "brew install anycable-go", abort_on_failure: true
244
+ run "anycable-go -v", abort_on_failure: true
245
+ end
246
+
247
+ def install_from_binary
248
+ bin_path ||= DEFAULT_BIN_PATH if options[:devenv] # User don't want interactive mode
249
+ bin_path ||= ask "Please, enter the path to download the AnyCable-Go binary to", default: DEFAULT_BIN_PATH, path: true
250
+
251
+ generate "anycable:download", download_options(bin_path: bin_path)
252
+
253
+ true
254
+ end
255
+
256
+ def download_options(**params)
257
+ opts = options.merge(params)
258
+ [].tap do |args|
259
+ args << "--os #{opts[:os]}" if opts[:os]
260
+ args << "--cpu #{opts[:cpu]}" if opts[:cpu]
261
+ args << "--bin-path=#{opts[:bin_path]}" if opts[:bin_path]
262
+ args << "--version #{opts[:version]}" if opts[:version]
263
+ end.join(" ")
264
+ end
265
+ end
266
+ end
@@ -0,0 +1,3 @@
1
+ server: bin/rails server
2
+ assets: bin/webpack-dev-server
3
+ anycable: bundle exec anycable --server-command "anycable-go --port 3334"
@@ -0,0 +1,43 @@
1
+ # This file contains per-environment settings for AnyCable.
2
+ #
3
+ # Since AnyCable config is based on anyway_config (https://github.com/palkan/anyway_config), all AnyCable settings
4
+ # can be set or overridden through the corresponding environment variables.
5
+ # E.g., `rpc_host` is overridden by ANYCABLE_RPC_HOST, `debug` by ANYCABLE_DEBUG etc.
6
+ #
7
+ # Note that AnyCable recognizes REDIS_URL env variable for Redis pub/sub adapter. If you want to
8
+ # use another Redis instance for AnyCable, provide ANYCABLE_REDIS_URL variable.
9
+ #
10
+ # Read more about AnyCable configuration here: <%= DOCS_ROOT %>/ruby/configuration
11
+ #
12
+ default: &default
13
+ # Turn on/off access logs ("Started..." and "Finished...")
14
+ access_logs_disabled: false
15
+ # Persist "dirty" session between RPC calls (might be required for apps using stimulus_reflex <3.0)
16
+ # persistent_session_enabled: true
17
+ # This is the host and the port to run AnyCable RPC server on.
18
+ # You must configure your WebSocket server to connect to it, e.g.:
19
+ # $ anycable-go --rpc-host="<rpc hostname>:50051"
20
+ rpc_host: "127.0.0.1:50051"
21
+ # Whether to enable gRPC level logging or not
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 -%>
30
+ # Use the same channel name for WebSocket server, e.g.:
31
+ # $ anycable-go --redis-channel="__anycable__"
32
+ redis_channel: "__anycable__"
33
+
34
+ development:
35
+ <<: *default
36
+ redis_url: "redis://localhost:6379/1"
37
+
38
+ production:
39
+ <<: *default
40
+ <%- if !redis? -%>
41
+ # Use Redis in production
42
+ broadcast_adapter: redis
43
+ <%- 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") %>
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add Warden middkeware to AnyCable stack to allow accessing
4
+ # Devise current user via `env["warden"].user`.
5
+ #
6
+ # See <%= DOCS_ROOT %>/ruby/authentication
7
+ AnyCable::Rails::Rack.middleware.use Warden::Manager do |config|
8
+ Devise.warden_config = config
9
+ end