anycable-rails 1.0.0.preview2 → 1.0.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 +4 -4
- data/CHANGELOG.md +28 -122
- data/README.md +14 -34
- data/lib/anycable/rails.rb +36 -2
- data/lib/anycable/rails/actioncable/channel.rb +4 -0
- data/lib/anycable/rails/actioncable/connection.rb +39 -35
- data/lib/anycable/rails/actioncable/connection/persistent_session.rb +7 -3
- data/lib/anycable/rails/actioncable/connection/serializable_identification.rb +42 -0
- data/lib/anycable/rails/actioncable/remote_connections.rb +11 -0
- data/lib/anycable/rails/actioncable/testing.rb +35 -0
- data/lib/anycable/rails/channel_state.rb +46 -0
- data/lib/anycable/rails/compatibility.rb +4 -7
- data/lib/anycable/rails/compatibility/rubocop.rb +0 -1
- data/lib/anycable/rails/compatibility/rubocop/config/default.yml +3 -0
- data/lib/anycable/rails/compatibility/rubocop/cops/anycable/instance_vars.rb +1 -1
- data/lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb +4 -4
- data/lib/anycable/rails/railtie.rb +26 -18
- data/lib/anycable/rails/refinements/subscriptions.rb +5 -0
- data/lib/anycable/rails/session_proxy.rb +19 -2
- data/lib/anycable/rails/version.rb +1 -1
- data/lib/generators/anycable/download/USAGE +14 -0
- data/lib/generators/anycable/download/download_generator.rb +83 -0
- data/lib/generators/anycable/setup/USAGE +2 -0
- data/lib/{rails/generators → generators}/anycable/setup/setup_generator.rb +100 -88
- data/lib/generators/anycable/setup/templates/Procfile.dev.tt +6 -0
- data/lib/{rails/generators → generators}/anycable/setup/templates/config/anycable.yml.tt +22 -4
- data/lib/generators/anycable/setup/templates/config/cable.yml.tt +11 -0
- data/lib/{rails/generators/anycable/setup/templates/config/initializers/anycable.rb → generators/anycable/setup/templates/config/initializers/anycable.rb.tt} +1 -1
- data/lib/generators/anycable/with_os_helpers.rb +55 -0
- metadata +29 -52
- data/lib/anycable/rails/compatibility/rubocop/cops/anycable/remote_disconnect.rb +0 -31
- data/lib/rails/generators/anycable/setup/templates/Procfile +0 -2
- data/lib/rails/generators/anycable/setup/templates/Procfile.dev +0 -3
- data/lib/rails/generators/anycable/setup/templates/bin/heroku-web +0 -7
- data/lib/rails/generators/anycable/setup/templates/config/cable.yml.tt +0 -11
@@ -15,16 +15,20 @@ module ActionCable
|
|
15
15
|
return super unless socket.session
|
16
16
|
|
17
17
|
super.tap do |req|
|
18
|
-
req.env[Rack::RACK_SESSION] =
|
19
|
-
AnyCable::Rails::SessionProxy.new(req.env[Rack::RACK_SESSION], socket.session)
|
18
|
+
req.env[::Rack::RACK_SESSION] =
|
19
|
+
AnyCable::Rails::SessionProxy.new(req.env[::Rack::RACK_SESSION], socket.session)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
23
|
def commit_session!
|
24
|
-
return unless request_loaded? && request.session.loaded?
|
24
|
+
return unless request_loaded? && request.session.respond_to?(:loaded?) && request.session.loaded?
|
25
25
|
|
26
26
|
socket.session = request.session.to_json
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
31
|
+
|
32
|
+
::ActionCable::Connection::Base.prepend(
|
33
|
+
::ActionCable::Connection::PersistentSession
|
34
|
+
)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Connection
|
5
|
+
module SerializableIdentification
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def identified_by(*identifiers)
|
10
|
+
super
|
11
|
+
Array(identifiers).each do |identifier|
|
12
|
+
define_method(identifier) do
|
13
|
+
instance_variable_get(:"@#{identifier}") || fetch_identifier(identifier)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Generate identifiers info.
|
20
|
+
# Converts GlobalID compatible vars to corresponding global IDs params.
|
21
|
+
def identifiers_hash
|
22
|
+
identifiers.each_with_object({}) do |id, acc|
|
23
|
+
obj = instance_variable_get("@#{id}")
|
24
|
+
next unless obj
|
25
|
+
|
26
|
+
acc[id] = AnyCable::Rails.serialize(obj)
|
27
|
+
end.compact
|
28
|
+
end
|
29
|
+
|
30
|
+
def identifiers_json
|
31
|
+
identifiers_hash.to_json
|
32
|
+
end
|
33
|
+
|
34
|
+
# Fetch identifier and deserialize if neccessary
|
35
|
+
def fetch_identifier(name)
|
36
|
+
@cached_ids[name] ||= @cached_ids.fetch(name) do
|
37
|
+
AnyCable::Rails.deserialize(ids[name.to_s])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable/remote_connections"
|
4
|
+
|
5
|
+
ActionCable::RemoteConnections::RemoteConnection.include(ActionCable::Connection::SerializableIdentification)
|
6
|
+
|
7
|
+
ActionCable::RemoteConnections::RemoteConnection.prepend(Module.new do
|
8
|
+
def disconnect(reconnect: true)
|
9
|
+
::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
|
10
|
+
end
|
11
|
+
end)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file contains patches to Action Cable testing modules
|
4
|
+
|
5
|
+
# Trigger autoload (if constant is defined)
|
6
|
+
begin
|
7
|
+
ActionCable::Channel::TestCase # rubocop:disable Lint/Void
|
8
|
+
ActionCable::Connection::TestCase # rubocop:disable Lint/Void
|
9
|
+
rescue NameError
|
10
|
+
return
|
11
|
+
end
|
12
|
+
|
13
|
+
ActionCable::Channel::ChannelStub.prepend(Module.new do
|
14
|
+
def subscribe_to_channel
|
15
|
+
# allocate @streams
|
16
|
+
streams
|
17
|
+
handle_subscribe
|
18
|
+
end
|
19
|
+
end)
|
20
|
+
|
21
|
+
ActionCable::Channel::ConnectionStub.prepend(Module.new do
|
22
|
+
def socket
|
23
|
+
@socket ||= AnyCable::Socket.new(env: {})
|
24
|
+
end
|
25
|
+
|
26
|
+
alias_method :anycable_socket, :socket
|
27
|
+
end)
|
28
|
+
|
29
|
+
ActionCable::Connection::TestConnection.prepend(Module.new do
|
30
|
+
def initialize(request)
|
31
|
+
@request = request
|
32
|
+
@cached_ids = {}
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module Rails
|
5
|
+
module ChannelState
|
6
|
+
module ClassMethods
|
7
|
+
def state_attr_accessor(*names)
|
8
|
+
names.each do |name|
|
9
|
+
channel_state_attributes << name
|
10
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
11
|
+
def #{name}
|
12
|
+
return @#{name} if instance_variable_defined?(:@#{name})
|
13
|
+
@#{name} = AnyCable::Rails.deserialize(connection.socket.istate["#{name}"], json: true) if connection.anycable_socket
|
14
|
+
end
|
15
|
+
|
16
|
+
def #{name}=(val)
|
17
|
+
connection.socket.istate["#{name}"] = AnyCable::Rails.serialize(val, json: true) if connection.anycable_socket
|
18
|
+
instance_variable_set(:@#{name}, val)
|
19
|
+
end
|
20
|
+
RUBY
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def channel_state_attributes
|
25
|
+
return @channel_state_attributes if instance_variable_defined?(:@channel_state_attributes)
|
26
|
+
|
27
|
+
@channel_state_attributes =
|
28
|
+
if superclass.respond_to?(:channel_state_attributes)
|
29
|
+
superclass.channel_state_attributes.dup
|
30
|
+
else
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.included(base)
|
37
|
+
base.extend ClassMethods
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
ActiveSupport.on_load(:action_cable) do
|
44
|
+
# `state_attr_accessor` must be available in Action Cable
|
45
|
+
::ActionCable::Channel::Base.include(AnyCable::Rails::ChannelState)
|
46
|
+
end
|
@@ -33,6 +33,10 @@ module AnyCable
|
|
33
33
|
res = yield
|
34
34
|
diff = instance_variables - was_ivars
|
35
35
|
|
36
|
+
if self.class.respond_to?(:channel_state_attributes)
|
37
|
+
diff.delete_if { |ivar| self.class.channel_state_attributes.include?(:"#{ivar.to_s.sub(/^@/, "")}") }
|
38
|
+
end
|
39
|
+
|
36
40
|
unless diff.empty?
|
37
41
|
raise AnyCable::CompatibilityError,
|
38
42
|
"Channel instance variables are not supported by AnyCable, " \
|
@@ -48,12 +52,5 @@ module AnyCable
|
|
48
52
|
raise AnyCable::CompatibilityError, "Periodical timers are not supported by AnyCable"
|
49
53
|
end
|
50
54
|
end)
|
51
|
-
|
52
|
-
ActionCable::RemoteConnections::RemoteConnection.prepend(Module.new do
|
53
|
-
def disconnect
|
54
|
-
raise AnyCable::CompatibilityError,
|
55
|
-
"Disconnecting remote clients is not supported by AnyCable yet"
|
56
|
-
end
|
57
|
-
end)
|
58
55
|
end
|
59
56
|
end
|
@@ -4,7 +4,6 @@ require "rubocop"
|
|
4
4
|
require "pathname"
|
5
5
|
|
6
6
|
require_relative "rubocop/cops/anycable/stream_from"
|
7
|
-
require_relative "rubocop/cops/anycable/remote_disconnect"
|
8
7
|
require_relative "rubocop/cops/anycable/periodical_timers"
|
9
8
|
require_relative "rubocop/cops/anycable/instance_vars"
|
10
9
|
|
@@ -25,7 +25,7 @@ module RuboCop
|
|
25
25
|
# end
|
26
26
|
#
|
27
27
|
class InstanceVars < RuboCop::Cop::Cop
|
28
|
-
MSG = "Channel instance variables are not supported in AnyCable"
|
28
|
+
MSG = "Channel instance variables are not supported in AnyCable. Use `state_attr_accessor` instead"
|
29
29
|
|
30
30
|
def on_class(node)
|
31
31
|
find_nested_ivars(node) do |nested_ivar|
|
@@ -11,7 +11,7 @@ module RuboCop
|
|
11
11
|
# # bad
|
12
12
|
# class MyChannel < ApplicationCable::Channel
|
13
13
|
# def follow
|
14
|
-
#
|
14
|
+
# stream_for(room) {}
|
15
15
|
# end
|
16
16
|
# end
|
17
17
|
#
|
@@ -36,15 +36,15 @@ module RuboCop
|
|
36
36
|
#
|
37
37
|
class StreamFrom < RuboCop::Cop::Cop
|
38
38
|
def_node_matcher :stream_from_with_block?, <<-PATTERN
|
39
|
-
(block (send _ :stream_from ...) ...)
|
39
|
+
(block {(send _ :stream_from ...) (send _ :stream_for ...)} ...)
|
40
40
|
PATTERN
|
41
41
|
|
42
42
|
def_node_matcher :stream_from_with_callback?, <<-PATTERN
|
43
|
-
(send _ :stream_from str_type? (block (send nil? :lambda) ...))
|
43
|
+
{(send _ :stream_from str_type? (block (send nil? :lambda) ...)) (send _ :stream_for ... (block (send nil? :lambda) ...))}
|
44
44
|
PATTERN
|
45
45
|
|
46
46
|
def_node_matcher :args_of_stream_from, <<-PATTERN
|
47
|
-
(send _ :stream_from str_type? $...)
|
47
|
+
{(send _ :stream_from str_type? $...) (send _ :stream_for $...)}
|
48
48
|
PATTERN
|
49
49
|
|
50
50
|
def_node_matcher :coder_symbol?, "(pair (sym :coder) ...)"
|
@@ -1,23 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "anycable/rails/channel_state"
|
4
|
+
|
3
5
|
module AnyCable
|
4
6
|
module Rails
|
5
7
|
class Railtie < ::Rails::Railtie # :nodoc:
|
6
8
|
initializer "anycable.disable_action_cable_mount", after: "action_cable.set_configs" do |app|
|
7
|
-
|
8
|
-
adapter = ::ActionCable.server.config.cable&.fetch("adapter", nil)
|
9
|
-
next unless AnyCable::Rails.compatible_adapter?(adapter)
|
9
|
+
next unless AnyCable::Rails.enabled?
|
10
10
|
|
11
11
|
app.config.action_cable.mount_path = nil
|
12
12
|
end
|
13
13
|
|
14
14
|
initializer "anycable.logger", after: "action_cable.logger" do |_app|
|
15
|
-
AnyCable.logger =
|
15
|
+
AnyCable.logger = ::ActionCable.server.config.logger
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
AnyCable.configure_server do
|
18
|
+
AnyCable.logger = ActiveSupport::TaggedLogging.new(::ActionCable.server.config.logger)
|
19
|
+
# Broadcast server logs to STDOUT in development
|
20
|
+
if ::Rails.env.development? &&
|
21
|
+
!ActiveSupport::Logger.logger_outputs_to?(::Rails.logger, STDOUT)
|
21
22
|
console = ActiveSupport::Logger.new(STDOUT)
|
22
23
|
console.formatter = ::Rails.logger.formatter
|
23
24
|
console.level = ::Rails.logger.level
|
@@ -42,23 +43,30 @@ module AnyCable
|
|
42
43
|
|
43
44
|
initializer "anycable.connection_factory", after: "action_cable.set_configs" do |app|
|
44
45
|
ActiveSupport.on_load(:action_cable) do
|
45
|
-
|
46
|
-
if
|
47
|
-
|
46
|
+
# Add AnyCable patch method stub (we use it in ChannelState to distinguish between Action Cable and AnyCable)
|
47
|
+
# NOTE: Method could be already defined if patch was loaded manually
|
48
|
+
ActionCable::Connection::Base.attr_reader(:anycable_socket) unless ActionCable::Connection::Base.method_defined?(:anycable_socket)
|
49
|
+
|
50
|
+
app.config.to_prepare do
|
51
|
+
AnyCable.connection_factory = ActionCable.server.config.connection_class.call
|
52
|
+
end
|
48
53
|
|
54
|
+
if AnyCable::Rails.enabled?
|
55
|
+
require "anycable/rails/actioncable/connection"
|
49
56
|
if AnyCable.config.persistent_session_enabled
|
50
57
|
require "anycable/rails/actioncable/connection/persistent_session"
|
51
|
-
::ActionCable::Connection::Base.prepend(
|
52
|
-
::ActionCable::Connection::PersistentSession
|
53
|
-
)
|
54
|
-
end
|
55
|
-
|
56
|
-
app.config.to_prepare do
|
57
|
-
AnyCable.connection_factory = ActionCable.server.config.connection_class.call
|
58
58
|
end
|
59
59
|
end
|
60
60
|
end
|
61
61
|
end
|
62
|
+
|
63
|
+
initializer "anycable.testing" do |app|
|
64
|
+
next unless ::Rails.env.test?
|
65
|
+
|
66
|
+
ActiveSupport.on_load(:action_cable) do
|
67
|
+
require "anycable/rails/actioncable/testing"
|
68
|
+
end
|
69
|
+
end
|
62
70
|
end
|
63
71
|
end
|
64
72
|
end
|
@@ -7,6 +7,11 @@ module AnyCable
|
|
7
7
|
# Find or add a subscription to the list
|
8
8
|
def fetch(identifier)
|
9
9
|
add("identifier" => identifier) unless subscriptions[identifier]
|
10
|
+
|
11
|
+
unless subscriptions[identifier]
|
12
|
+
raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}"
|
13
|
+
end
|
14
|
+
|
10
15
|
subscriptions[identifier]
|
11
16
|
end
|
12
17
|
end
|
@@ -5,8 +5,6 @@ module AnyCable
|
|
5
5
|
# Wrap `request.session` to lazily load values provided
|
6
6
|
# in the RPC call (set by the previous calls)
|
7
7
|
class SessionProxy
|
8
|
-
delegate_missing_to :@rack_session
|
9
|
-
|
10
8
|
attr_reader :rack_session, :socket_session
|
11
9
|
|
12
10
|
def initialize(rack_session, socket_session)
|
@@ -41,6 +39,25 @@ module AnyCable
|
|
41
39
|
rack_session.keys + socket_session.keys
|
42
40
|
end
|
43
41
|
|
42
|
+
# Delegate both publuc and private methods to rack_session
|
43
|
+
def respond_to_missing?(name, include_private = false)
|
44
|
+
return false if name == :marshal_dump || name == :_dump
|
45
|
+
rack_session.respond_to?(name, include_private) || super
|
46
|
+
end
|
47
|
+
|
48
|
+
def method_missing(method, *args, &block)
|
49
|
+
if rack_session.respond_to?(method, true)
|
50
|
+
rack_session.send(method, *args, &block)
|
51
|
+
else
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# This method is used by StimulusReflex to obtain `@by`
|
57
|
+
def instance_variable_get(name)
|
58
|
+
super || rack_session.instance_variable_get(name)
|
59
|
+
end
|
60
|
+
|
44
61
|
private
|
45
62
|
|
46
63
|
def restore!
|
@@ -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
|