anycable-rails 0.6.5 → 1.0.0.rc3

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