anycable-rails-core 1.4.0

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +203 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +106 -0
  5. data/lib/action_cable/subscription_adapter/any_cable.rb +40 -0
  6. data/lib/action_cable/subscription_adapter/anycable.rb +10 -0
  7. data/lib/anycable/rails/action_cable_ext/channel.rb +51 -0
  8. data/lib/anycable/rails/action_cable_ext/connection.rb +90 -0
  9. data/lib/anycable/rails/action_cable_ext/remote_connections.rb +13 -0
  10. data/lib/anycable/rails/channel_state.rb +108 -0
  11. data/lib/anycable/rails/compatibility/rubocop/config/default.yml +14 -0
  12. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/instance_vars.rb +50 -0
  13. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/periodical_timers.rb +29 -0
  14. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb +100 -0
  15. data/lib/anycable/rails/compatibility/rubocop.rb +27 -0
  16. data/lib/anycable/rails/compatibility.rb +63 -0
  17. data/lib/anycable/rails/config.rb +19 -0
  18. data/lib/anycable/rails/connection.rb +211 -0
  19. data/lib/anycable/rails/connection_factory.rb +44 -0
  20. data/lib/anycable/rails/connections/persistent_session.rb +40 -0
  21. data/lib/anycable/rails/connections/serializable_identification.rb +46 -0
  22. data/lib/anycable/rails/connections/session_proxy.rb +81 -0
  23. data/lib/anycable/rails/middlewares/executor.rb +31 -0
  24. data/lib/anycable/rails/middlewares/log_tagging.rb +21 -0
  25. data/lib/anycable/rails/rack.rb +56 -0
  26. data/lib/anycable/rails/railtie.rb +92 -0
  27. data/lib/anycable/rails/version.rb +7 -0
  28. data/lib/anycable/rails.rb +76 -0
  29. data/lib/anycable-rails.rb +3 -0
  30. data/lib/generators/anycable/download/USAGE +14 -0
  31. data/lib/generators/anycable/download/download_generator.rb +85 -0
  32. data/lib/generators/anycable/setup/USAGE +2 -0
  33. data/lib/generators/anycable/setup/setup_generator.rb +300 -0
  34. data/lib/generators/anycable/setup/templates/Procfile.dev.tt +6 -0
  35. data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +48 -0
  36. data/lib/generators/anycable/setup/templates/config/cable.yml.tt +11 -0
  37. data/lib/generators/anycable/setup/templates/config/initializers/anycable.rb.tt +9 -0
  38. data/lib/generators/anycable/with_os_helpers.rb +55 -0
  39. metadata +128 -0
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ module Connections
6
+ # Wrap `request.session` to lazily load values provided
7
+ # in the RPC call (set by the previous calls)
8
+ class SessionProxy
9
+ attr_reader :rack_session, :socket_session
10
+
11
+ def initialize(rack_session, socket_session)
12
+ @rack_session = rack_session
13
+ @socket_session = JSON.parse(socket_session).with_indifferent_access
14
+ end
15
+
16
+ %i[has_key? [] []= fetch delete dig].each do |mid|
17
+ class_eval <<~CODE, __FILE__, __LINE__ + 1
18
+ def #{mid}(*args, **kwargs, &block)
19
+ restore_key! args.first
20
+ rack_session.#{mid}(*args, **kwargs, &block)
21
+ end
22
+ CODE
23
+ end
24
+
25
+ alias_method :include?, :has_key?
26
+ alias_method :key?, :has_key?
27
+
28
+ %i[update merge! to_hash].each do |mid|
29
+ class_eval <<~CODE, __FILE__, __LINE__ + 1
30
+ def #{mid}(*args, **kwargs, &block)
31
+ restore!
32
+ rack_session.#{mid}(*args, **kwargs, &block)
33
+ end
34
+ CODE
35
+ end
36
+
37
+ alias_method :to_h, :to_hash
38
+
39
+ def keys
40
+ rack_session.keys + socket_session.keys
41
+ end
42
+
43
+ # Delegate both publuc and private methods to rack_session
44
+ def respond_to_missing?(name, include_private = false)
45
+ return false if name == :marshal_dump || name == :_dump
46
+ rack_session.respond_to?(name, include_private) || super
47
+ end
48
+
49
+ def method_missing(method, *args, &block)
50
+ if rack_session.respond_to?(method, true)
51
+ rack_session.send(method, *args, &block)
52
+ else
53
+ super
54
+ end
55
+ end
56
+
57
+ # This method is used by StimulusReflex to obtain `@by`
58
+ def instance_variable_get(name)
59
+ super || rack_session.instance_variable_get(name)
60
+ end
61
+
62
+ private
63
+
64
+ def restore!
65
+ socket_session.keys.each(&method(:restore_key!))
66
+ end
67
+
68
+ def restore_key!(key)
69
+ return unless socket_session.key?(key)
70
+ val = socket_session.delete(key)
71
+ rack_session[key] =
72
+ if val.is_a?(String)
73
+ GlobalID::Locator.locate(val) || val
74
+ else
75
+ val
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ module Middlewares
6
+ # Executor runs Rails executor for each call
7
+ # See https://guides.rubyonrails.org/v5.2.0/threading_and_code_execution.html#framework-behavior
8
+ class Executor < AnyCable::Middleware
9
+ attr_reader :executor
10
+
11
+ def initialize(executor)
12
+ @executor = executor
13
+ end
14
+
15
+ def call(method, message, metadata)
16
+ if ::Rails.respond_to?(:error)
17
+ executor.wrap do
18
+ sid = metadata["sid"]
19
+
20
+ ::Rails.error.record(context: {method: method, payload: message.to_h, sid: sid}) do
21
+ yield
22
+ end
23
+ end
24
+ else
25
+ executor.wrap { yield }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ module Middlewares
6
+ # Middleware to add `sid` (session ID) tag to logs.
7
+ #
8
+ # Session ID could be provided through gRPC metadata `sid` key.
9
+ #
10
+ # See https://github.com/grpc/grpc-go/blob/master/Documentation/grpc-metadata.md
11
+ class LogTagging < AnyCable::Middleware
12
+ def call(_method, _request, metadata)
13
+ sid = metadata["sid"]
14
+ return yield unless sid
15
+
16
+ AnyCable.logger.tagged("AnyCable sid=#{sid}") { yield }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/configuration"
4
+ require "action_dispatch/middleware/stack"
5
+
6
+ module AnyCable
7
+ module Rails
8
+ # Rack middleware stack to modify the HTTP request object.
9
+ #
10
+ # AnyCable Websocket server does not use Rack middleware processing mechanism (which Rails uses
11
+ # when Action Cable is mounted into the main app).
12
+ #
13
+ # Some middlewares could enhance request env with useful information.
14
+ #
15
+ # For instance, consider the Rails session middleware: it's responsible for restoring the
16
+ # session data from cookies.
17
+ #
18
+ # AnyCable adds session middelware by default to its own stack.
19
+ #
20
+ # You can also use any Rack/Rails middleware you want. For example, to enable Devise/Warden
21
+ # you can add the following code to an initializer or any other configuration file:
22
+ #
23
+ # AnyCable::Rails::Rack.middleware.use Warden::Manager do |config|
24
+ # Devise.warden_config = config
25
+ # end
26
+ module Rack
27
+ def self.app_build_lock
28
+ @app_build_lock
29
+ end
30
+
31
+ @app_build_lock = Mutex.new
32
+
33
+ def self.middleware
34
+ @middleware ||= ::Rails::Configuration::MiddlewareStackProxy.new
35
+ end
36
+
37
+ def self.default_middleware_stack
38
+ config = ::Rails.application.config
39
+
40
+ ActionDispatch::MiddlewareStack.new do |middleware|
41
+ middleware.use(config.session_store, config.session_options) if config.session_store
42
+ end
43
+ end
44
+
45
+ def self.app
46
+ @rack_app || app_build_lock.synchronize do
47
+ @rack_app ||= default_middleware_stack.yield_self do |stack|
48
+ middleware.merge_into(stack)
49
+ end.yield_self do |stack|
50
+ stack.build { [-1, {}, []] }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/rails/action_cable_ext/connection"
4
+ require "anycable/rails/action_cable_ext/channel"
5
+ require "anycable/rails/action_cable_ext/remote_connections"
6
+
7
+ require "anycable/rails/channel_state"
8
+ require "anycable/rails/connection_factory"
9
+
10
+ module AnyCable
11
+ module Rails
12
+ class Railtie < ::Rails::Railtie # :nodoc:
13
+ initializer "anycable.disable_action_cable_mount", after: "action_cable.set_configs" do |app|
14
+ next unless AnyCable::Rails.enabled?
15
+
16
+ app.config.action_cable.mount_path = nil
17
+ end
18
+
19
+ initializer "anycable.logger", after: "action_cable.logger" do |_app|
20
+ AnyCable.logger = ::ActionCable.server.config.logger
21
+
22
+ AnyCable.configure_server do
23
+ server_logger = AnyCable.logger = ::ActionCable.server.config.logger
24
+ AnyCable.logger = ActiveSupport::TaggedLogging.new(server_logger) if server_logger.is_a?(::Logger)
25
+ # Broadcast server logs to STDOUT in development
26
+ if ::Rails.env.development? &&
27
+ !ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, $stdout)
28
+ console = ActiveSupport::Logger.new($stdout)
29
+ console.formatter = ::Rails.logger.formatter if ::Rails.logger.respond_to?(:formatter)
30
+ console.level = ::Rails.logger.level if ::Rails.logger.respond_to?(:level)
31
+ AnyCable.logger.extend(ActiveSupport::Logger.broadcast(console))
32
+ end
33
+ end
34
+
35
+ # Add tagging middleware
36
+ if AnyCable.logger.respond_to?(:tagged)
37
+ require "anycable/rails/middlewares/log_tagging"
38
+
39
+ AnyCable.middleware.use(AnyCable::Rails::Middlewares::LogTagging)
40
+ end
41
+ end
42
+
43
+ initializer "anycable.executor" do |app|
44
+ require "anycable/rails/middlewares/executor"
45
+ # see https://github.com/rails/rails/pull/33469/files
46
+ executor = app.config.reload_classes_only_on_change ? app.reloader : app.executor
47
+ AnyCable.middleware.use(AnyCable::Rails::Middlewares::Executor.new(executor))
48
+
49
+ if app.executor.respond_to?(:error_reporter)
50
+ AnyCable.capture_exception do |ex, method, message|
51
+ ::Rails.error.report(ex, handled: false, context: {method: method.to_sym, payload: message})
52
+ end
53
+ end
54
+ end
55
+
56
+ initializer "anycable.connection_factory", after: "action_cable.set_configs" do |app|
57
+ ActiveSupport.on_load(:action_cable) do
58
+ app.config.to_prepare do
59
+ AnyCable.connection_factory = AnyCable::Rails::ConnectionFactory.new
60
+ end
61
+
62
+ if AnyCable.config.persistent_session_enabled?
63
+ require "anycable/rails/connections/persistent_session"
64
+ end
65
+ end
66
+ end
67
+
68
+ initializer "anycable.routes" do
69
+ next unless AnyCable::Rails.enabled?
70
+
71
+ config.after_initialize do |app|
72
+ config = AnyCable.config
73
+ unless config.http_rpc_mount_path.nil?
74
+ app.routes.prepend do
75
+ mount AnyCable::HTTRPC::Server.new => config.http_rpc_mount_path, :internal => true
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ # Since Rails 6.1
82
+ if respond_to?(:server)
83
+ server do
84
+ next unless AnyCable.config.embedded? && AnyCable::Rails.enabled?
85
+
86
+ require "anycable/cli"
87
+ AnyCable::CLI.embed!
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyCable
4
+ module Rails
5
+ VERSION = "1.4.0"
6
+ end
7
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable"
4
+ require "anycable/rails/version"
5
+ require "anycable/rails/config"
6
+ require "anycable/rails/rack"
7
+
8
+ require "globalid"
9
+
10
+ module AnyCable
11
+ # Rails handler for AnyCable
12
+ module Rails
13
+ require "anycable/rails/railtie"
14
+
15
+ ADAPTER_ALIASES = %w[any_cable anycable].freeze
16
+
17
+ class << self
18
+ def enabled?
19
+ adapter = ::ActionCable.server.config.cable&.fetch("adapter", nil)
20
+ compatible_adapter?(adapter)
21
+ end
22
+
23
+ def compatible_adapter?(adapter)
24
+ ADAPTER_ALIASES.include?(adapter)
25
+ end
26
+
27
+ # Serialize connection/channel state variable to string
28
+ # using GlobalID where possible or JSON (if json: true)
29
+ def serialize(obj, json: false)
30
+ obj.try(:to_gid_param) || (json ? obj.to_json : obj)
31
+ end
32
+
33
+ # Deserialize previously serialized value from string to
34
+ # Ruby object.
35
+ # If the resulting object is a Hash, make it indifferent
36
+ def deserialize(str, json: false)
37
+ str.yield_self do |val|
38
+ next val unless val.is_a?(String)
39
+
40
+ gval = GlobalID::Locator.locate(val)
41
+ return gval if gval
42
+
43
+ next val unless json
44
+
45
+ JSON.parse(val)
46
+ end.yield_self do |val|
47
+ next val.with_indifferent_access if val.is_a?(Hash)
48
+ val
49
+ end
50
+ end
51
+
52
+ module Extension
53
+ def broadcast(channel, payload)
54
+ super
55
+ ::AnyCable.broadcast(channel, payload)
56
+ end
57
+ end
58
+
59
+ def extend_adapter!(adapter)
60
+ adapter.extend(Extension)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Warn if application has been already initialized.
67
+ # AnyCable should be loaded before initialization in order to work correctly.
68
+ if defined?(::Rails) && ::Rails.application && ::Rails.application.initialized?
69
+ puts("\n**************************************************")
70
+ puts(
71
+ "⛔️ WARNING: AnyCable loaded after application initialization. Might not work correctly.\n" \
72
+ "Please, make sure to remove `require: false` in your Gemfile or " \
73
+ "require manually in `environment.rb` before `Rails.application.initialize!`"
74
+ )
75
+ puts("**************************************************\n\n")
76
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/rails"
@@ -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,85 @@
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
+ FileUtils.mkdir_p(to) unless File.directory?(to)
65
+
66
+ run "#{sudo(to)}curl -L #{url} -o #{file_path}", abort_on_failure: true
67
+ run "#{sudo(to)}chmod +x #{file_path}", abort_on_failure: true
68
+ run "#{file_path} -v", abort_on_failure: true
69
+ end
70
+
71
+ def sudo(path)
72
+ sudo = ""
73
+ unless File.writable?(path)
74
+ if yes? "Path is not writable 😕. Do you have sudo privileges?"
75
+ sudo = "sudo "
76
+ else
77
+ say_status :error, "❌ Failed to install AnyCable-Go WebSocket server", :red
78
+ raise StandardError, "Path #{path} is not writable!"
79
+ end
80
+ end
81
+
82
+ sudo
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,2 @@
1
+ Description:
2
+ Configures your application to work with AnyCable interactively.