anycable-rails 0.6.3 → 1.0.0.rc1

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 +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