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