anycable-rails 0.6.4 → 1.0.0.rc2

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -102
  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 +4 -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 +7 -10
  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 +258 -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 +42 -28
  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.4"
5
+ VERSION = "1.0.0.rc2"
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,258 @@
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
+ say_status :help, "️️⚠️ Docker development configuration could vary", :yellow
163
+
164
+ say "Here is an example snippet for docker-compose.yml:"
165
+ say <<~YML
166
+ ─────────────────────────────────────────
167
+ ws:
168
+ image: anycable/anycable-go:1.0
169
+ ports:
170
+ - '8080:8080'
171
+ environment:
172
+ ANYCABLE_HOST: "0.0.0.0"
173
+ ANYCABLE_REDIS_URL: redis://redis:6379/0
174
+ ANYCABLE_RPC_HOST: anycable:50051
175
+ ANYCABLE_DEBUG: 1
176
+ depends_on:
177
+ - anycable
178
+ - redis
179
+
180
+ anycable:
181
+ <<: *backend
182
+ command: bundle exec anycable
183
+ environment:
184
+ <<: *backend_environment
185
+ ANYCABLE_REDIS_URL: redis://redis:6379/0
186
+ ANYCABLE_RPC_HOST: 0.0.0.0:50051
187
+ ports:
188
+ - '50051'
189
+ ─────────────────────────────────────────
190
+ YML
191
+ end
192
+
193
+ def install_for_local
194
+ install_server
195
+ template_proc_files
196
+ end
197
+
198
+ def install_server
199
+ answer = SERVER_SOURCES.index(options[:source]) || 99
200
+
201
+ until SERVER_SOURCES[answer.to_i]
202
+ answer = ask "How do you want to install AnyCable-Go WebSocket server? (1) Homebrew, (2) Download binary, (0) Skip"
203
+ end
204
+
205
+ case answer.to_i
206
+ when 0
207
+ say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 #{DOCS_ROOT}/anycable-go/getting_started", :yellow
208
+ return
209
+ else
210
+ return unless send("install_from_#{SERVER_SOURCES[answer.to_i]}")
211
+ end
212
+
213
+ say_status :info, "✅ AnyCable-Go WebSocket server has been successfully installed"
214
+ end
215
+
216
+ def template_proc_files
217
+ file_name = "Procfile.dev"
218
+
219
+ if File.exist?(file_name)
220
+ append_file file_name, 'anycable: bundle exec anycable --server-command "anycable-go --port 3334"'
221
+ else
222
+ say_status :help, "💡 We recommend using Hivemind to manage multiple processes in development 👉 https://github.com/DarthSim/hivemind", :yellow
223
+
224
+ if options[:skip_procfile_dev].nil?
225
+ return unless yes? "Do you want to create a '#{file_name}' file?"
226
+ elsif options[:skip_procfile_dev]
227
+ return
228
+ end
229
+
230
+ template file_name
231
+ end
232
+ end
233
+
234
+ def install_from_brew
235
+ run "brew install anycable-go", abort_on_failure: true
236
+ run "anycable-go -v", abort_on_failure: true
237
+ end
238
+
239
+ def install_from_binary
240
+ bin_path ||= DEFAULT_BIN_PATH if options[:devenv] # User don't want interactive mode
241
+ bin_path ||= ask "Please, enter the path to download the AnyCable-Go binary to", default: DEFAULT_BIN_PATH, path: true
242
+
243
+ generate "anycable:download", download_options(bin_path: bin_path)
244
+
245
+ true
246
+ end
247
+
248
+ def download_options(**params)
249
+ opts = options.merge(params)
250
+ [].tap do |args|
251
+ args << "--os #{opts[:os]}" if opts[:os]
252
+ args << "--cpu #{opts[:cpu]}" if opts[:cpu]
253
+ args << "--bin-path=#{opts[:bin_path]}" if opts[:bin_path]
254
+ args << "--version #{opts[:version]}" if opts[:version]
255
+ end.join(" ")
256
+ end
257
+ end
258
+ 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
@@ -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