anycable-rails-core 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +203 -0
- data/MIT-LICENSE +20 -0
- data/README.md +106 -0
- data/lib/action_cable/subscription_adapter/any_cable.rb +40 -0
- data/lib/action_cable/subscription_adapter/anycable.rb +10 -0
- data/lib/anycable/rails/action_cable_ext/channel.rb +51 -0
- data/lib/anycable/rails/action_cable_ext/connection.rb +90 -0
- data/lib/anycable/rails/action_cable_ext/remote_connections.rb +13 -0
- data/lib/anycable/rails/channel_state.rb +108 -0
- data/lib/anycable/rails/compatibility/rubocop/config/default.yml +14 -0
- data/lib/anycable/rails/compatibility/rubocop/cops/anycable/instance_vars.rb +50 -0
- data/lib/anycable/rails/compatibility/rubocop/cops/anycable/periodical_timers.rb +29 -0
- data/lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb +100 -0
- data/lib/anycable/rails/compatibility/rubocop.rb +27 -0
- data/lib/anycable/rails/compatibility.rb +63 -0
- data/lib/anycable/rails/config.rb +19 -0
- data/lib/anycable/rails/connection.rb +211 -0
- data/lib/anycable/rails/connection_factory.rb +44 -0
- data/lib/anycable/rails/connections/persistent_session.rb +40 -0
- data/lib/anycable/rails/connections/serializable_identification.rb +46 -0
- data/lib/anycable/rails/connections/session_proxy.rb +81 -0
- data/lib/anycable/rails/middlewares/executor.rb +31 -0
- data/lib/anycable/rails/middlewares/log_tagging.rb +21 -0
- data/lib/anycable/rails/rack.rb +56 -0
- data/lib/anycable/rails/railtie.rb +92 -0
- data/lib/anycable/rails/version.rb +7 -0
- data/lib/anycable/rails.rb +76 -0
- data/lib/anycable-rails.rb +3 -0
- data/lib/generators/anycable/download/USAGE +14 -0
- data/lib/generators/anycable/download/download_generator.rb +85 -0
- data/lib/generators/anycable/setup/USAGE +2 -0
- data/lib/generators/anycable/setup/setup_generator.rb +300 -0
- data/lib/generators/anycable/setup/templates/Procfile.dev.tt +6 -0
- data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +48 -0
- data/lib/generators/anycable/setup/templates/config/cable.yml.tt +11 -0
- data/lib/generators/anycable/setup/templates/config/initializers/anycable.rb.tt +9 -0
- data/lib/generators/anycable/with_os_helpers.rb +55 -0
- 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,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,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
|