anycable-rails 0.6.3 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -100
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +37 -36
  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 +7 -0
  8. data/lib/anycable/rails/actioncable/connection.rb +70 -50
  9. data/lib/anycable/rails/actioncable/connection/persistent_session.rb +30 -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/channel_state.rb +48 -0
  13. data/lib/anycable/rails/compatibility.rb +10 -11
  14. data/lib/anycable/rails/compatibility/rubocop.rb +0 -1
  15. data/lib/anycable/rails/compatibility/rubocop/config/default.yml +3 -1
  16. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/instance_vars.rb +1 -1
  17. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb +4 -4
  18. data/lib/anycable/rails/config.rb +8 -4
  19. data/lib/anycable/rails/rack.rb +56 -0
  20. data/lib/anycable/rails/railtie.rb +17 -10
  21. data/lib/anycable/rails/refinements/subscriptions.rb +5 -0
  22. data/lib/anycable/rails/session_proxy.rb +79 -0
  23. data/lib/anycable/rails/version.rb +1 -1
  24. data/lib/generators/anycable/download/USAGE +14 -0
  25. data/lib/generators/anycable/download/download_generator.rb +77 -0
  26. data/lib/generators/anycable/setup/USAGE +2 -0
  27. data/lib/generators/anycable/setup/setup_generator.rb +246 -0
  28. data/lib/generators/anycable/setup/templates/Procfile.dev +3 -0
  29. data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +43 -0
  30. data/lib/generators/anycable/setup/templates/config/cable.yml.tt +11 -0
  31. data/lib/generators/anycable/setup/templates/config/initializers/anycable.rb.tt +9 -0
  32. data/lib/generators/anycable/with_os_helpers.rb +55 -0
  33. metadata +48 -43
  34. 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.3"
5
+ VERSION = "1.0.0.rc1"
6
6
  end
7
7
  end
@@ -0,0 +1,14 @@
1
+ Description:
2
+ Install AnyCable-Go web server.
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,77 @@
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
+ # TODO: change to latest release
13
+ VERSION = "1.0.0.preview1"
14
+
15
+ class_option :bin_path,
16
+ type: :string,
17
+ desc: "Where to download AnyCable-Go server binary (default: #{DEFAULT_BIN_PATH})"
18
+ class_option :version,
19
+ type: :string,
20
+ desc: "Specify the AnyCable-Go version (defaults to latest release)"
21
+
22
+ def download_bin
23
+ out = options[:bin_path] || DEFAULT_BIN_PATH
24
+ version = options[:version] || VERSION
25
+
26
+ download_exe(
27
+ release_url(version),
28
+ to: out,
29
+ file_name: "anycable-go"
30
+ )
31
+
32
+ true
33
+ end
34
+
35
+ private
36
+
37
+ def release_url(version)
38
+ if Gem::Version.new(version).segments.first >= 1
39
+ new_release_url(version)
40
+ else
41
+ legacy_release_url(version)
42
+ end
43
+ end
44
+
45
+ def legacy_release_url(version)
46
+ "https://github.com/anycable/anycable-go/releases/download/v#{version}/" \
47
+ "anycable-go-v#{version}-#{os_name}-#{cpu_name}"
48
+ end
49
+
50
+ def new_release_url(version)
51
+ "https://github.com/anycable/anycable-go/releases/download/v#{version}/" \
52
+ "anycable-go-#{os_name}-#{cpu_name}"
53
+ end
54
+
55
+ def download_exe(url, to:, file_name:)
56
+ file_path = File.join(to, file_name)
57
+
58
+ run "#{sudo(to)}curl -L #{url} -o #{file_path}", abort_on_failure: true
59
+ run "#{sudo(to)}chmod +x #{file_path}", abort_on_failure: true
60
+ run "#{file_path} -v", abort_on_failure: true
61
+ end
62
+
63
+ def sudo(path)
64
+ sudo = ""
65
+ unless File.writable?(path)
66
+ if yes? "Path is not writable 😕. Do you have sudo privileges?"
67
+ sudo = "sudo "
68
+ else
69
+ say_status :error, "❌ Failed to install AnyCable-Go WebSocket server", :red
70
+ raise StandardError, "Path #{path} is not writable!"
71
+ end
72
+ end
73
+
74
+ sudo
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,2 @@
1
+ Description:
2
+ Configures your application to work with AnyCable interactively.
@@ -0,0 +1,246 @@
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.action_cable.url = ENV.fetch("CABLE_URL", "ws://localhost:8080/cable") if AnyCable::Rails.enabled?
53
+ SNIPPET
54
+ end
55
+
56
+ environment(nil, env: :production) do
57
+ <<~SNIPPET
58
+ # Specify AnyCable WebSocket server URL to use by JS client
59
+ config.action_cable.url = ENV.fetch("CABLE_URL") if AnyCable::Rails.enabled?
60
+ SNIPPET
61
+ end
62
+
63
+ say_status :info, "✅ 'config.action_cable.url' has been configured"
64
+ say_status :help, "⚠️ If you're using JS client make sure you have " \
65
+ "`action_cable_meta_tag` included before any <script> tag in your application.html"
66
+ end
67
+
68
+ def development_method
69
+ answer = DEVELOPMENT_METHODS.index(options[:devenv]) || 99
70
+
71
+ until DEVELOPMENT_METHODS[answer.to_i]
72
+ answer = ask "Which environment do you use for development? (1) Local, (2) Docker, (0) Skip"
73
+ end
74
+
75
+ case env = DEVELOPMENT_METHODS[answer.to_i]
76
+ when "skip"
77
+ say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 #{DOCS_ROOT}/anycable-go/getting_started", :yellow
78
+ else
79
+ send "install_for_#{env}"
80
+ end
81
+ end
82
+
83
+ def heroku
84
+ if options[:skip_heroku].nil?
85
+ return unless yes? "Do you use Heroku for deployment? [Yn]"
86
+ elsif options[:skip_heroku]
87
+ return
88
+ end
89
+
90
+ in_root do
91
+ next unless File.file?("Procfile")
92
+
93
+ contents = File.read("Procfile")
94
+ contents.sub!(/^web: (.*)$/, %q(web: [[ "$ANYCABLE_DEPLOYMENT" == "true" ]] && bundle exec anycable --server-command="anycable-go" || \1))
95
+ File.write("Procfile", contents)
96
+ say_status :info, "✅ Procfile updated"
97
+ end
98
+
99
+ say_status :help, "️️⚠️ Please, read the required steps to configure Heroku applications 👉 #{DOCS_ROOT}/deployment/heroku", :yellow
100
+ end
101
+
102
+ def devise
103
+ in_root do
104
+ return unless File.file?("config/initializers/devise.rb")
105
+ end
106
+
107
+ inside("config/initializers") do
108
+ template "anycable.rb"
109
+ end
110
+
111
+ say_status :info, "✅ config/initializers/anycable.rb with Devise configuration has been added"
112
+ end
113
+
114
+ def rubocop_compatibility
115
+ return unless rubocop?
116
+
117
+ say_status :info, "🤖 Running static compatibility checks with RuboCop"
118
+ res = run "bundle exec rubocop -r 'anycable/rails/compatibility/rubocop' --only AnyCable/InstanceVars,AnyCable/PeriodicalTimers,AnyCable/InstanceVars"
119
+ say_status :help, "⚠️ Please, take a look at the icompatibilities above and fix them. See https://docs.anycable.io/v1/#/ruby/compatibility" unless res
120
+ end
121
+
122
+ def finish
123
+ say_status :info, "✅ AnyCable has been configured successfully!"
124
+ end
125
+
126
+ private
127
+
128
+ def stimulus_reflex?
129
+ !!gemfile_lock&.match?(/^\s+stimulus_reflex\b/)
130
+ end
131
+
132
+ def redis?
133
+ !!gemfile_lock&.match?(/^\s+redis\b/)
134
+ end
135
+
136
+ def rubocop?
137
+ !!gemfile_lock&.match?(/^\s+rubocop\b/)
138
+ end
139
+
140
+ def gemfile_lock
141
+ @gemfile_lock ||= begin
142
+ res = nil
143
+ in_root do
144
+ next unless File.file?("Gemfile.lock")
145
+ res = File.read("Gemfile.lock")
146
+ end
147
+ res
148
+ end
149
+ end
150
+
151
+ def install_for_docker
152
+ say_status :help, "️️⚠️ Docker development configuration could vary", :yellow
153
+
154
+ say "Here is an example snippet for docker-compose.yml:"
155
+ say <<~YML
156
+ ─────────────────────────────────────────
157
+ ws:
158
+ image: anycable/anycable-go:1.0.0.preview1
159
+ ports:
160
+ - '8080:8080'
161
+ environment:
162
+ ANYCABLE_REDIS_URL: redis://redis:6379/0
163
+ ANYCABLE_RPC_HOST: anycable:50051
164
+ depends_on:
165
+ - anycable-rpc
166
+ - redis
167
+
168
+ anycable:
169
+ <<: *backend
170
+ command: bundle exec anycable
171
+ environment:
172
+ <<: *backend_environment
173
+ ANYCABLE_REDIS_URL: redis://redis:6379/0
174
+ ANYCABLE_RPC_HOST: 0.0.0.0:50051
175
+ ports:
176
+ - '50051'
177
+ ─────────────────────────────────────────
178
+ YML
179
+ end
180
+
181
+ def install_for_local
182
+ install_server
183
+ template_proc_files
184
+ end
185
+
186
+ def install_server
187
+ answer = SERVER_SOURCES.index(options[:source]) || 99
188
+
189
+ until SERVER_SOURCES[answer.to_i]
190
+ answer = ask "How do you want to install AnyCable-Go WebSocket server? (1) Homebrew, (2) Download binary, (0) Skip"
191
+ end
192
+
193
+ case answer.to_i
194
+ when 0
195
+ say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 #{DOCS_ROOT}/anycable-go/getting_started", :yellow
196
+ return
197
+ else
198
+ return unless send("install_from_#{SERVER_SOURCES[answer.to_i]}")
199
+ end
200
+
201
+ say_status :info, "✅ AnyCable-Go WebSocket server has been successfully installed"
202
+ end
203
+
204
+ def template_proc_files
205
+ file_name = "Procfile.dev"
206
+
207
+ if File.exist?(file_name)
208
+ append_file file_name, 'anycable: bundle exec anycable --server-command "anycable-go --port 3334"'
209
+ else
210
+ say_status :help, "💡 We recommend using Hivemind to manage multiple processes in development 👉 https://github.com/DarthSim/hivemind", :yellow
211
+
212
+ if options[:skip_procfile_dev].nil?
213
+ return unless yes? "Do you want to create a '#{file_name}' file?"
214
+ elsif options[:skip_procfile_dev]
215
+ return
216
+ end
217
+
218
+ template file_name
219
+ end
220
+ end
221
+
222
+ def install_from_brew
223
+ run "brew install anycable-go", abort_on_failure: true
224
+ run "anycable-go -v", abort_on_failure: true
225
+ end
226
+
227
+ def install_from_binary
228
+ bin_path ||= DEFAULT_BIN_PATH if options[:devenv] # User don't want interactive mode
229
+ bin_path ||= ask "Please, enter the path to download the AnyCable-Go binary to", default: DEFAULT_BIN_PATH, path: true
230
+
231
+ generate "anycable:download", download_options(bin_path: bin_path)
232
+
233
+ true
234
+ end
235
+
236
+ def download_options(**params)
237
+ opts = options.merge(params)
238
+ [].tap do |args|
239
+ args << "--os #{opts[:os]}" if opts[:os]
240
+ args << "--cpu #{opts[:cpu]}" if opts[:cpu]
241
+ args << "--bin-path=#{opts[:bin_path]}" if opts[:bin_path]
242
+ args << "--version #{opts[:version]}" if opts[:version]
243
+ end.join(" ")
244
+ end
245
+ end
246
+ 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 StimulusReflex apps)
16
+ persistent_session_enabled: <%= stimulus_reflex? %>
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
@@ -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