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