anycable-rails 0.5.5 → 0.6.0.rc1
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/.github/ISSUE_TEMPLATE.md +29 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +31 -0
- data/.rubocop.yml +24 -34
- data/CHANGELOG.md +31 -1
- data/README.md +47 -92
- data/Rakefile +7 -2
- data/anycable-rails.gemspec +3 -3
- data/lib/action_cable/subscription_adapter/any_cable.rb +23 -0
- data/lib/anycable/rails.rb +4 -4
- data/lib/anycable/rails/actioncable/channel.rb +8 -4
- data/lib/anycable/rails/actioncable/connection.rb +57 -23
- data/lib/anycable/rails/compatibility.rb +57 -0
- data/lib/anycable/rails/compatibility/rubocop.rb +28 -0
- data/lib/anycable/rails/compatibility/rubocop/config/default.yml +12 -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/remote_disconnect.rb +31 -0
- data/lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb +100 -0
- data/lib/anycable/rails/config.rb +3 -2
- data/lib/anycable/rails/middlewares/executor.rb +21 -0
- data/lib/anycable/rails/middlewares/log_tagging.rb +21 -0
- data/lib/anycable/rails/railtie.rb +20 -27
- data/lib/anycable/rails/refinements/subscriptions.rb +1 -1
- data/lib/anycable/rails/version.rb +2 -2
- metadata +25 -19
- data/lib/anycable/rails/actioncable/server.rb +0 -14
- data/lib/anycable/rails/activerecord/release_connection.rb +0 -29
- data/lib/generators/anycable/USAGE +0 -7
- data/lib/generators/anycable/anycable_generator.rb +0 -16
- data/lib/generators/anycable/templates/anycable.yml +0 -41
- data/lib/generators/anycable/templates/script +0 -6
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable-rails"
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module SubscriptionAdapter
|
7
|
+
# AnyCable subscription adapter delegates broadcasts
|
8
|
+
# to AnyCable
|
9
|
+
class AnyCable < Base
|
10
|
+
def initialize(*); end
|
11
|
+
|
12
|
+
def broadcast(channel, payload)
|
13
|
+
::AnyCable.broadcast(channel, payload)
|
14
|
+
end
|
15
|
+
|
16
|
+
def shutdown
|
17
|
+
# nothing to do
|
18
|
+
# we only need this method for development,
|
19
|
+
# 'cause code reloading triggers `server.restart` -> `pubsub.shutdown`
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/anycable/rails.rb
CHANGED
@@ -4,17 +4,17 @@ require "anycable"
|
|
4
4
|
require "anycable/rails/version"
|
5
5
|
require "anycable/rails/config"
|
6
6
|
|
7
|
-
module
|
7
|
+
module AnyCable
|
8
8
|
# Rails handler for AnyCable
|
9
9
|
module Rails
|
10
10
|
require "anycable/rails/railtie"
|
11
|
-
|
12
|
-
require "anycable/rails/actioncable/connection"
|
11
|
+
# Load Action Cable patches only when running RPC server
|
12
|
+
require "anycable/rails/actioncable/connection" if defined?(::AnyCable::CLI)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
16
|
# Warn if application has been already initialized.
|
17
|
-
#
|
17
|
+
# AnyCable should be loaded before initialization in order to work correctly.
|
18
18
|
if defined?(::Rails) && ::Rails.application && ::Rails.application.initialized?
|
19
19
|
puts("\n**************************************************")
|
20
20
|
puts(
|
@@ -13,11 +13,15 @@ module ActionCable
|
|
13
13
|
# noop
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
def start_periodic_timers
|
17
|
+
# noop
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop_periodic_timers
|
21
|
+
# noop
|
22
|
+
end
|
20
23
|
|
24
|
+
def stream_from(broadcasting, _callback = nil, _options = {})
|
21
25
|
connection.socket.subscribe identifier, broadcasting
|
22
26
|
end
|
23
27
|
|
@@ -6,9 +6,13 @@ require "anycable/rails/actioncable/channel"
|
|
6
6
|
|
7
7
|
module ActionCable
|
8
8
|
module Connection
|
9
|
-
# rubocop:disable Metrics/ClassLength
|
9
|
+
# rubocop: disable Metrics/ClassLength
|
10
10
|
class Base # :nodoc:
|
11
|
-
|
11
|
+
# We store logger tags in identifiers to be able
|
12
|
+
# to re-use them in the subsequent calls
|
13
|
+
LOG_TAGS_IDENTIFIER = "__ltags__"
|
14
|
+
|
15
|
+
using AnyCable::Refinements::Subscriptions
|
12
16
|
|
13
17
|
attr_reader :socket
|
14
18
|
|
@@ -27,9 +31,11 @@ module ActionCable
|
|
27
31
|
end
|
28
32
|
end
|
29
33
|
|
30
|
-
def initialize(socket, identifiers:
|
34
|
+
def initialize(socket, identifiers: "{}", subscriptions: [])
|
31
35
|
@ids = ActiveSupport::JSON.decode(identifiers)
|
32
36
|
|
37
|
+
@ltags = ids.delete(LOG_TAGS_IDENTIFIER)
|
38
|
+
|
33
39
|
@cached_ids = {}
|
34
40
|
@env = socket.env
|
35
41
|
@coder = ActiveSupport::JSON
|
@@ -43,11 +49,12 @@ module ActionCable
|
|
43
49
|
def handle_open
|
44
50
|
logger.info started_request_message if access_logs?
|
45
51
|
|
52
|
+
verify_origin!
|
53
|
+
|
46
54
|
connect if respond_to?(:connect)
|
47
55
|
send_welcome_message
|
48
56
|
rescue ActionCable::Connection::Authorization::UnauthorizedError
|
49
|
-
|
50
|
-
close
|
57
|
+
reject_request
|
51
58
|
end
|
52
59
|
|
53
60
|
def handle_close
|
@@ -88,12 +95,14 @@ module ActionCable
|
|
88
95
|
# Generate identifiers info.
|
89
96
|
# Converts GlobalID compatible vars to corresponding global IDs params.
|
90
97
|
def identifiers_hash
|
91
|
-
|
98
|
+
obj = { LOG_TAGS_IDENTIFIER => fetch_ltags }
|
99
|
+
|
100
|
+
identifiers.each_with_object(obj) do |id, acc|
|
92
101
|
obj = instance_variable_get("@#{id}")
|
93
102
|
next unless obj
|
94
103
|
|
95
104
|
acc[id] = obj.try(:to_gid_param) || obj
|
96
|
-
end
|
105
|
+
end.compact
|
97
106
|
end
|
98
107
|
|
99
108
|
def identifiers_json
|
@@ -103,7 +112,7 @@ module ActionCable
|
|
103
112
|
# Fetch identifier and deserialize if neccessary
|
104
113
|
def fetch_identifier(name)
|
105
114
|
@cached_ids[name] ||= @cached_ids.fetch(name) do
|
106
|
-
val =
|
115
|
+
val = ids[name.to_s]
|
107
116
|
next val unless val.is_a?(String)
|
108
117
|
|
109
118
|
GlobalID::Locator.locate(val) || val
|
@@ -111,32 +120,57 @@ module ActionCable
|
|
111
120
|
end
|
112
121
|
|
113
122
|
def logger
|
114
|
-
|
123
|
+
@logger ||= TaggedLoggerProxy.new(AnyCable.logger, tags: ltags || [])
|
115
124
|
end
|
116
125
|
|
117
126
|
private
|
118
127
|
|
128
|
+
attr_reader :ids, :ltags
|
129
|
+
|
119
130
|
def started_request_message
|
120
|
-
|
121
|
-
|
122
|
-
" [
|
123
|
-
|
124
|
-
Time.now.to_s
|
125
|
-
]
|
131
|
+
format(
|
132
|
+
'Started "%s"%s for %s at %s',
|
133
|
+
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
|
134
|
+
)
|
126
135
|
end
|
127
136
|
|
128
137
|
def finished_request_message(reason = "Closed")
|
129
|
-
|
130
|
-
|
131
|
-
" [
|
132
|
-
|
133
|
-
Time.now.to_s,
|
134
|
-
reason
|
135
|
-
]
|
138
|
+
format(
|
139
|
+
'Finished "%s"%s for %s at %s (%s)',
|
140
|
+
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s, reason
|
141
|
+
)
|
136
142
|
end
|
137
143
|
|
138
144
|
def access_logs?
|
139
|
-
|
145
|
+
AnyCable.config.access_logs_disabled == false
|
146
|
+
end
|
147
|
+
|
148
|
+
def fetch_ltags
|
149
|
+
if instance_variable_defined?(:@logger)
|
150
|
+
logger.tags
|
151
|
+
else
|
152
|
+
ltags
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def server
|
157
|
+
ActionCable.server
|
158
|
+
end
|
159
|
+
|
160
|
+
def verify_origin!
|
161
|
+
return unless socket.env.key?("HTTP_ORIGIN")
|
162
|
+
|
163
|
+
return if allow_request_origin?
|
164
|
+
|
165
|
+
raise(
|
166
|
+
ActionCable::Connection::Authorization::UnauthorizedError,
|
167
|
+
"Origin is not allowed"
|
168
|
+
)
|
169
|
+
end
|
170
|
+
|
171
|
+
def reject_request
|
172
|
+
logger.info finished_request_message("Rejected") if access_logs?
|
173
|
+
close
|
140
174
|
end
|
141
175
|
end
|
142
176
|
# rubocop:enable Metrics/ClassLength
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
class CompatibilityError < StandardError; end
|
5
|
+
|
6
|
+
module Compatibility # :nodoc:
|
7
|
+
ActionCable::Channel::Base.prepend(Module.new do
|
8
|
+
def stream_from(broadcasting, callback = nil, coder: nil)
|
9
|
+
if coder.present? && coder != ActiveSupport::JSON
|
10
|
+
raise AnyCable::CompatibilityError, "Custom coders are not supported by AnyCable"
|
11
|
+
end
|
12
|
+
|
13
|
+
if callback.present? || block_given?
|
14
|
+
raise AnyCable::CompatibilityError,
|
15
|
+
"Custom stream callbacks are not supported by AnyCable"
|
16
|
+
end
|
17
|
+
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
%w[subscribe_to_channel perform_action].each do |mid|
|
22
|
+
module_eval <<~CODE, __FILE__, __LINE__ + 1
|
23
|
+
def #{mid}(*)
|
24
|
+
__anycable_check_ivars__ { super }
|
25
|
+
end
|
26
|
+
CODE
|
27
|
+
end
|
28
|
+
|
29
|
+
def __anycable_check_ivars__
|
30
|
+
was_ivars = instance_variables
|
31
|
+
res = yield
|
32
|
+
diff = instance_variables - was_ivars
|
33
|
+
|
34
|
+
unless diff.empty?
|
35
|
+
raise AnyCable::CompatibilityError,
|
36
|
+
"Channel instance variables are not supported by AnyCable, " \
|
37
|
+
"but were set: #{diff.join(', ')}"
|
38
|
+
end
|
39
|
+
|
40
|
+
res
|
41
|
+
end
|
42
|
+
end)
|
43
|
+
|
44
|
+
ActionCable::Channel::Base.singleton_class.prepend(Module.new do
|
45
|
+
def periodically(*)
|
46
|
+
raise AnyCable::CompatibilityError, "Periodical timers are not supported by AnyCable"
|
47
|
+
end
|
48
|
+
end)
|
49
|
+
|
50
|
+
ActionCable::RemoteConnections::RemoteConnection.prepend(Module.new do
|
51
|
+
def disconnect
|
52
|
+
raise AnyCable::CompatibilityError,
|
53
|
+
"Disconnecting remote clients is not supported by AnyCable yet"
|
54
|
+
end
|
55
|
+
end)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
require_relative "rubocop/cops/anycable/stream_from"
|
7
|
+
require_relative "rubocop/cops/anycable/remote_disconnect"
|
8
|
+
require_relative "rubocop/cops/anycable/periodical_timers"
|
9
|
+
require_relative "rubocop/cops/anycable/instance_vars"
|
10
|
+
|
11
|
+
module RuboCop
|
12
|
+
module AnyCable # :nodoc:
|
13
|
+
CONFIG_DEFAULT = Pathname.new(__dir__).join("rubocop", "config", "default.yml").freeze
|
14
|
+
|
15
|
+
# Merge anycable config into default configuration
|
16
|
+
# See https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
|
17
|
+
def self.inject!
|
18
|
+
path = CONFIG_DEFAULT.to_s
|
19
|
+
puts "configuration from #{path}" if ConfigLoader.debug?
|
20
|
+
hash = ConfigLoader.send(:load_yaml_configuration, path)
|
21
|
+
config = Config.new(hash, path)
|
22
|
+
config = ConfigLoader.merge_with_default(config, path)
|
23
|
+
ConfigLoader.instance_variable_set(:@default_configuration, config)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
RuboCop::AnyCable.inject!
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module AnyCable
|
8
|
+
# Checks for instance variable usage inside subscriptions.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# class MyChannel < ApplicationCable::Channel
|
13
|
+
# def subscribed
|
14
|
+
# @post = Post.find(params[:id])
|
15
|
+
# stream_from @post
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # good
|
20
|
+
# class MyChannel < ApplicationCable::Channel
|
21
|
+
# def subscribed
|
22
|
+
# post = Post.find(params[:id])
|
23
|
+
# stream_from post
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
class InstanceVars < RuboCop::Cop::Cop
|
28
|
+
MSG = "Channel instance variables are not supported in AnyCable"
|
29
|
+
|
30
|
+
def on_class(node)
|
31
|
+
find_nested_ivars(node) do |nested_ivar|
|
32
|
+
add_offense(nested_ivar)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def find_nested_ivars(node, &block)
|
39
|
+
node.each_child_node do |child|
|
40
|
+
if child.begin_type? || child.block_type? || child.def_type?
|
41
|
+
find_nested_ivars(child, &block)
|
42
|
+
elsif child.ivasgn_type? || child.ivar_type?
|
43
|
+
yield(child)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module AnyCable
|
8
|
+
# Checks for periodical timers usage.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# class MyChannel < ApplicationCable::Channel
|
13
|
+
# periodically(:do_something, every: 2.seconds)
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
class PeriodicalTimers < RuboCop::Cop::Cop
|
17
|
+
MSG = "Periodical Timers are not supported in AnyCable"
|
18
|
+
|
19
|
+
def_node_matcher :calls_periodically?, <<-PATTERN
|
20
|
+
(send _ :periodically ...)
|
21
|
+
PATTERN
|
22
|
+
|
23
|
+
def on_send(node)
|
24
|
+
add_offense(node) if calls_periodically?(node)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module AnyCable
|
8
|
+
# Checks for remote disconnect usage.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# class MyServive
|
13
|
+
# def call(user)
|
14
|
+
# ActionCable.server.remote_connections.where(current_user: user).disconnect
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
class RemoteDisconnect < RuboCop::Cop::Cop
|
19
|
+
MSG = "Disconnecting remote clients is not supported in AnyCable"
|
20
|
+
|
21
|
+
def_node_matcher :has_remote_disconnect?, <<-PATTERN
|
22
|
+
(send (send (send _ :remote_connections) ...) :disconnect)
|
23
|
+
PATTERN
|
24
|
+
|
25
|
+
def on_send(node)
|
26
|
+
add_offense(node) if has_remote_disconnect?(node)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module AnyCable
|
8
|
+
# Checks for #stream_from calls with custom callbacks or coders.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# class MyChannel < ApplicationCable::Channel
|
13
|
+
# def follow
|
14
|
+
# stream_from("all") {}
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# class MyChannel < ApplicationCable::Channel
|
19
|
+
# def follow
|
20
|
+
# stream_from("all", -> {})
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# class MyChannel < ApplicationCable::Channel
|
25
|
+
# def follow
|
26
|
+
# stream_from("all", coder: SomeCoder)
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# # good
|
31
|
+
# class MyChannel < ApplicationCable::Channel
|
32
|
+
# def follow
|
33
|
+
# stream_from "all"
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
class StreamFrom < RuboCop::Cop::Cop
|
38
|
+
def_node_matcher :stream_from_with_block?, <<-PATTERN
|
39
|
+
(block (send _ :stream_from ...) ...)
|
40
|
+
PATTERN
|
41
|
+
|
42
|
+
def_node_matcher :stream_from_with_callback?, <<-PATTERN
|
43
|
+
(send _ :stream_from str_type? (block (send nil? :lambda) ...))
|
44
|
+
PATTERN
|
45
|
+
|
46
|
+
def_node_matcher :args_of_stream_from, <<-PATTERN
|
47
|
+
(send _ :stream_from str_type? $...)
|
48
|
+
PATTERN
|
49
|
+
|
50
|
+
def_node_matcher :coder_symbol?, "(pair (sym :coder) ...)"
|
51
|
+
|
52
|
+
def_node_matcher :active_support_json?, <<-PATTERN
|
53
|
+
(pair _ (const (const nil? :ActiveSupport) :JSON))
|
54
|
+
PATTERN
|
55
|
+
|
56
|
+
def on_block(node)
|
57
|
+
add_callback_offense(node) if stream_from_with_block?(node)
|
58
|
+
end
|
59
|
+
|
60
|
+
def on_send(node)
|
61
|
+
if stream_from_with_callback?(node)
|
62
|
+
add_callback_offense(node)
|
63
|
+
return
|
64
|
+
end
|
65
|
+
|
66
|
+
args = args_of_stream_from(node)
|
67
|
+
find_coders(args) { |coder| add_custom_coder_offense(coder) }
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def find_coders(args)
|
73
|
+
return if args.nil?
|
74
|
+
|
75
|
+
args.select(&:hash_type?).each do |arg|
|
76
|
+
arg.each_child_node do |pair|
|
77
|
+
yield(pair) if coder_symbol?(pair) && !active_support_json?(pair)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_callback_offense(node)
|
83
|
+
add_offense(
|
84
|
+
node,
|
85
|
+
location: :expression,
|
86
|
+
message: "Custom stream callbacks are not supported in AnyCable"
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_custom_coder_offense(node)
|
91
|
+
add_offense(
|
92
|
+
node,
|
93
|
+
location: :expression,
|
94
|
+
message: "Custom coders are not supported in AnyCable"
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|