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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +29 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +31 -0
  4. data/.rubocop.yml +24 -34
  5. data/CHANGELOG.md +31 -1
  6. data/README.md +47 -92
  7. data/Rakefile +7 -2
  8. data/anycable-rails.gemspec +3 -3
  9. data/lib/action_cable/subscription_adapter/any_cable.rb +23 -0
  10. data/lib/anycable/rails.rb +4 -4
  11. data/lib/anycable/rails/actioncable/channel.rb +8 -4
  12. data/lib/anycable/rails/actioncable/connection.rb +57 -23
  13. data/lib/anycable/rails/compatibility.rb +57 -0
  14. data/lib/anycable/rails/compatibility/rubocop.rb +28 -0
  15. data/lib/anycable/rails/compatibility/rubocop/config/default.yml +12 -0
  16. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/instance_vars.rb +50 -0
  17. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/periodical_timers.rb +29 -0
  18. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/remote_disconnect.rb +31 -0
  19. data/lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb +100 -0
  20. data/lib/anycable/rails/config.rb +3 -2
  21. data/lib/anycable/rails/middlewares/executor.rb +21 -0
  22. data/lib/anycable/rails/middlewares/log_tagging.rb +21 -0
  23. data/lib/anycable/rails/railtie.rb +20 -27
  24. data/lib/anycable/rails/refinements/subscriptions.rb +1 -1
  25. data/lib/anycable/rails/version.rb +2 -2
  26. metadata +25 -19
  27. data/lib/anycable/rails/actioncable/server.rb +0 -14
  28. data/lib/anycable/rails/activerecord/release_connection.rb +0 -29
  29. data/lib/generators/anycable/USAGE +0 -7
  30. data/lib/generators/anycable/anycable_generator.rb +0 -16
  31. data/lib/generators/anycable/templates/anycable.yml +0 -41
  32. 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
@@ -4,17 +4,17 @@ require "anycable"
4
4
  require "anycable/rails/version"
5
5
  require "anycable/rails/config"
6
6
 
7
- module Anycable
7
+ module AnyCable
8
8
  # Rails handler for AnyCable
9
9
  module Rails
10
10
  require "anycable/rails/railtie"
11
- require "anycable/rails/actioncable/server"
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
- # Anycable should be loaded before initialization in order to work correctly.
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 stream_from(broadcasting, callback = nil, coder: nil)
17
- if callback.present? || coder.present? || block_given?
18
- raise ArgumentError, 'Custom stream callbacks are not supported in AnyCable!'
19
- end
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
- using Anycable::Refinements::Subscriptions
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: '{}', subscriptions: [])
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
- logger.info finished_request_message('Rejected') if access_logs?
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
- identifiers.each_with_object({}) do |id, acc|
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 = @ids[name.to_s]
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
- Anycable.logger
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
- 'Started "%s"%s for %s at %s' % [
121
- request.filtered_path,
122
- " [Anycable]",
123
- request.ip,
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
- 'Finished "%s"%s for %s at %s (%s)' % [
130
- request.filtered_path,
131
- " [Anycable]",
132
- request.ip,
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
- Anycable.config.access_logs_disabled == false
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,12 @@
1
+ AnyCable/InstanceVars:
2
+ Include:
3
+ - "**/channels/**/*.rb"
4
+
5
+ AnyCable/StreamFrom:
6
+ Include:
7
+ - "**/channels/**/*.rb"
8
+
9
+ AnyCable/PeriodicalTimers:
10
+ Include:
11
+ - "**/channels/**/*.rb"
12
+
@@ -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