anycable-rails 0.5.5 → 0.6.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|