anycable-rails-core 1.5.3 → 1.5.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +4 -8
  4. data/lib/anycable/rails/action_cable_ext/channel.rb +20 -0
  5. data/lib/anycable/rails/action_cable_ext/remote_connections.rb +4 -14
  6. data/lib/anycable/rails/compatibility/rubocop.rb +2 -25
  7. data/lib/anycable/rails/compatibility.rb +1 -0
  8. data/lib/anycable/rails/connection_factory.rb +20 -1
  9. data/lib/anycable/rails/connections/persistent_session.rb +5 -0
  10. data/lib/anycable/rails/connections/serializable_identification.rb +5 -0
  11. data/lib/anycable/rails/connections/session_proxy.rb +2 -2
  12. data/lib/anycable/rails/ext/jwt.rb +1 -1
  13. data/lib/anycable/rails/ext/signed_streams.rb +21 -0
  14. data/lib/anycable/rails/ext/whisper.rb +34 -0
  15. data/lib/anycable/rails/ext.rb +4 -1
  16. data/lib/anycable/rails/next/action_cable_ext/channel.rb +36 -0
  17. data/lib/anycable/rails/next/action_cable_ext/connection.rb +29 -0
  18. data/lib/anycable/rails/next/connection/persistent_session.rb +39 -0
  19. data/lib/anycable/rails/next/connection.rb +226 -0
  20. data/lib/anycable/rails/railtie.rb +0 -6
  21. data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/instance_vars.rb +1 -1
  22. data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/periodical_timers.rb +1 -1
  23. data/lib/anycable/rails/{compatibility/rubocop → rubocop}/cops/anycable/stream_from.rb +3 -5
  24. data/lib/anycable/rails/rubocop.rb +27 -0
  25. data/lib/anycable/rails/socket_id_tracking.rb +2 -2
  26. data/lib/anycable/rails/version.rb +1 -1
  27. data/lib/anycable/rails.rb +1 -1
  28. data/lib/generators/anycable/setup/setup_generator.rb +5 -8
  29. data/lib/generators/anycable/setup/templates/Procfile.dev.tt +1 -1
  30. data/lib/generators/anycable/setup/templates/anycable.toml.tt +83 -0
  31. data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +6 -2
  32. metadata +18 -11
  33. data/lib/anycable/rails/action_cable_ext/signed_streams.rb +0 -31
  34. /data/lib/anycable/rails/{compatibility/rubocop → rubocop}/config/default.yml +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae33f5aaf1057a1aa63e16280048477ed7f6be5ff403e7a45bd4198aed3ef226
4
- data.tar.gz: dfedd48dbeddaf2e722093bec52f459c576c59a3c549e26afe00753ee770496b
3
+ metadata.gz: 2d1806c2fa82dabe3ba59ed53b814f9b1380e42f8f6341af82c4c27bf192c283
4
+ data.tar.gz: f2bf0761e8d2edf39f9a17869f89e0e2ad0bb80d49c74de451f53b00a0406144
5
5
  SHA512:
6
- metadata.gz: 3e84c51f6fb7b2c7e7a1b1535b2f99b4db0d83ee3235b45104aad07e353924bf2ce91269b4dbb9b992f356a622594f3c2e44941edf60b47be899196ee8adfd83
7
- data.tar.gz: 6b9b15397056c29d3cbf01b566ea4282e41dc2065f8af827bb73f102c47e78d429aecbfd11d687660eb4c820225fa3411d9bd7bd4fb39a75155568a0e4e24e05
6
+ metadata.gz: a4ece01b56c5118971493acf71a9f99651961d872dc51a6f58808ce4daf84e3e9c9514f4d79b214cf4621b39131d2cbf0cac04a085cef0e0394bb72a7a11b4b8
7
+ data.tar.gz: '0441569abd1b2c94969fc30d2f4b2a33657a663f3e3b6a2e68c7970da1a2008f72d7fd4afe2de49d86b1d85963237f2630e459e2708cf19d8e9f3f105bf59f8a'
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.5.5 (2024-12-12)
6
+
7
+ - Publish RuboCop cops as a separate gem (`rubocop-anycable-rails`). ([@palkan][])
8
+
9
+ - Upgrade RuboCop cops. ([@palkan][])
10
+
11
+ ## 1.5.4 (2024-10-08)
12
+
13
+ - Add [actioncable-next](https://github.com/anycable/actioncable-next) support. ([@palkan][])
14
+
15
+ - Generate `anycable.toml` in `anycable:setup` generator. ([@palkan][])
16
+
5
17
  ## 1.5.3 (2024-09-12)
6
18
 
7
19
  - Set upper limit on supported Rails versions. ([@palkan][])
data/README.md CHANGED
@@ -8,8 +8,6 @@ AnyCable allows you to use any WebSocket server (written in any language) as a r
8
8
 
9
9
  With AnyCable you can use channels, client-side JS, broadcasting - (almost) all that you can do with Action Cable.
10
10
 
11
- You can even use Action Cable in development and not be afraid of [compatibility issues](#compatibility).
12
-
13
11
  💾 [Example Application](https://github.com/anycable/anycable_rails_demo)
14
12
 
15
13
  📑 [Documentation](https://docs.anycable.io/rails/getting_started).
@@ -21,9 +19,10 @@ You can even use Action Cable in development and not be afraid of [compatibility
21
19
 
22
20
  ## Requirements
23
21
 
24
- - Ruby >= 2.6
25
- - Rails >= 6.0 (Rails 5.1 could work but we're no longer enforce compatibility on CI)
26
- - Redis (see [other options](https://github.com/anycable/anycable/issues/2) for broadcasting)
22
+ - Ruby >= 3.1
23
+ - Rails >= 6.0\*
24
+
25
+ \* Recent `anycable-rails` versions only work with Rails 8+; older versions compatible with Rails 6 and Rails 7 still receive fixes and minor updates (patch releases).
27
26
 
28
27
  ## Usage
29
28
 
@@ -31,9 +30,6 @@ Add `anycable-rails` gem to your Gemfile:
31
30
 
32
31
  ```ruby
33
32
  gem "anycable-rails"
34
-
35
- # when using Redis broadcast adapter
36
- gem "redis", ">= 4.0"
37
33
  ```
38
34
 
39
35
  ### Interactive set up
@@ -57,3 +57,23 @@ ActionCable::Channel::Base.prepend(Module.new do
57
57
  connection.instance_variable_defined?(:@anycable_socket)
58
58
  end
59
59
  end)
60
+
61
+ # Handle $pubsub channel in Subscriptions
62
+ ActionCable::Connection::Subscriptions.prepend(Module.new do
63
+ # The contents are mostly copied from the original,
64
+ # there is no good way to configure channels mapping due to #safe_constantize
65
+ # and the layers of JSON
66
+ # https://github.com/rails/rails/blob/main/actioncable/lib/action_cable/connection/subscriptions.rb
67
+ def add(data)
68
+ id_key = data["identifier"]
69
+ id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
70
+
71
+ return if subscriptions.key?(id_key)
72
+
73
+ return super unless id_options[:channel] == "$pubsub"
74
+
75
+ subscription = AnyCable::Rails::PubSubChannel.new(connection, id_key, id_options)
76
+ subscriptions[id_key] = subscription
77
+ subscription.subscribe_to_channel
78
+ end
79
+ end)
@@ -3,20 +3,10 @@
3
3
  require "action_cable/remote_connections"
4
4
 
5
5
  ActionCable::RemoteConnections::RemoteConnection.include(AnyCable::Rails::Connections::SerializableIdentification)
6
-
7
6
  ActionCable::RemoteConnections::RemoteConnection.prepend(Module.new do
8
- # Rails <7.1 has no `reconnect` argument
9
- if ActionCable::RemoteConnections::RemoteConnection.instance_method(:disconnect).arity == 0
10
- def disconnect(reconnect: true)
11
- # Legacy Action Cable functionality if case we're not fully migrated yet
12
- super() unless AnyCable::Rails.enabled?
13
- ::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
14
- end
15
- else
16
- def disconnect(reconnect: true)
17
- # Legacy Action Cable functionality if case we're not fully migrated yet
18
- super unless AnyCable::Rails.enabled?
19
- ::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
20
- end
7
+ def disconnect(reconnect: true)
8
+ # Legacy Action Cable functionality if case we're not fully migrated yet
9
+ super unless AnyCable::Rails.enabled?
10
+ ::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
21
11
  end
22
12
  end)
@@ -1,27 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rubocop"
4
- require "pathname"
5
-
6
- require_relative "rubocop/cops/anycable/stream_from"
7
- require_relative "rubocop/cops/anycable/periodical_timers"
8
- require_relative "rubocop/cops/anycable/instance_vars"
9
-
10
- module RuboCop
11
- module AnyCable # :nodoc:
12
- CONFIG_DEFAULT = Pathname.new(__dir__).join("rubocop", "config", "default.yml").freeze
13
-
14
- # Merge anycable config into default configuration
15
- # See https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
16
- def self.inject!
17
- path = CONFIG_DEFAULT.to_s
18
- puts "configuration from #{path}" if ConfigLoader.debug?
19
- hash = ConfigLoader.send(:load_yaml_configuration, path)
20
- config = Config.new(hash, path)
21
- config = ConfigLoader.merge_with_default(config, path)
22
- ConfigLoader.instance_variable_set(:@default_configuration, config)
23
- end
24
- end
25
- end
26
-
27
- RuboCop::AnyCable.inject!
3
+ # For backwards compatibility
4
+ require_relative "../rubocop"
@@ -8,6 +8,7 @@ module AnyCable
8
8
  @active_periodic_timers
9
9
  @_streams
10
10
  @parameter_filter
11
+ @whisper_stream
11
12
  ]
12
13
 
13
14
  ActionCable::Channel::Base.prepend(Module.new do
@@ -1,6 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "anycable/rails/connection"
3
+ require "action_cable"
4
+
5
+ begin
6
+ ActionCable::Server::Socket
7
+ rescue
8
+ end
9
+
10
+ if defined?(ActionCable::Server::Socket)
11
+ require "anycable/rails/next/connection"
12
+ require "anycable/rails/next/action_cable_ext/connection"
13
+ require "anycable/rails/next/action_cable_ext/channel"
14
+ else
15
+ require "anycable/rails/connection"
16
+
17
+ require "anycable/rails/action_cable_ext/connection"
18
+ require "anycable/rails/action_cable_ext/channel"
19
+ end
20
+
21
+ require "anycable/rails/action_cable_ext/remote_connections"
22
+ require "anycable/rails/action_cable_ext/broadcast_options"
4
23
 
5
24
  module AnyCable
6
25
  module Rails
@@ -2,6 +2,11 @@
2
2
 
3
3
  require "anycable/rails/connections/session_proxy"
4
4
 
5
+ if defined?(ActionCable::Server::Socket)
6
+ require "anycable/rails/next/connection/persistent_session"
7
+ return
8
+ end
9
+
5
10
  module AnyCable
6
11
  module Rails
7
12
  module Connections
@@ -32,6 +32,11 @@ module AnyCable
32
32
  identifiers_hash.to_json
33
33
  end
34
34
 
35
+ def identifiers_json=(val)
36
+ @cached_ids = {}
37
+ @serialized_ids = val ? ActiveSupport::JSON.decode(val) : {}
38
+ end
39
+
35
40
  # Fetch identifier and deserialize if neccessary
36
41
  def fetch_identifier(name)
37
42
  return unless @cached_ids
@@ -46,9 +46,9 @@ module AnyCable
46
46
  rack_session.respond_to?(name, include_private) || super
47
47
  end
48
48
 
49
- def method_missing(method, *args, &block)
49
+ def method_missing(method, ...)
50
50
  if rack_session.respond_to?(method, true)
51
- rack_session.send(method, *args, &block)
51
+ rack_session.send(method, ...)
52
52
  else
53
53
  super
54
54
  end
@@ -10,7 +10,7 @@ module AnyCable
10
10
  super
11
11
  rescue AnyCable::JWT::ExpiredSignature
12
12
  logger.error "An expired JWT token was rejected"
13
- close(reason: "token_expired", reconnect: false) if websocket&.alive?
13
+ close(reason: "token_expired", reconnect: false)
14
14
  end
15
15
 
16
16
  def anycable_jwt_present?
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+
5
+ ActionCable::Connection::Base.include(Module.new do
6
+ # This method is assumed to be overriden in the connection class to enable public
7
+ # streams
8
+ def allow_public_streams?
9
+ false
10
+ end
11
+ end)
12
+
13
+ # Handle $pubsub channel in Subscriptions
14
+ ActionCable::Connection::Subscriptions.prepend(Module.new do
15
+ def subscription_from_identifier(id_key)
16
+ id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
17
+ return super unless id_options[:channel] == "$pubsub"
18
+
19
+ AnyCable::Rails::PubSubChannel.new(connection, id_key, id_options)
20
+ end
21
+ end)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+
5
+ ActionCable::Channel::Base.prepend(Module.new do
6
+ attr_accessor :whisper_stream
7
+
8
+ def stream_from(broadcasting, _callback = nil, **opts)
9
+ whispering = opts.delete(:whisper)
10
+ whispers_to(broadcasting) if whispering
11
+ super
12
+ end
13
+
14
+ def whispers_to(broadcasting)
15
+ logger.debug "#{self.class.name} whispers to #{broadcasting}"
16
+ self.whisper_stream = broadcasting
17
+ end
18
+ end)
19
+
20
+ ActionCable::Connection::Subscriptions.prepend(Module.new do
21
+ def execute_command(data)
22
+ return whisper(data) if data["command"] == "whisper"
23
+
24
+ super
25
+ end
26
+
27
+ def whisper(data)
28
+ subscription = find(data)
29
+ stream = subscription.whisper_stream
30
+ raise "Whispering stream is not set" unless stream
31
+
32
+ ::ActionCable.server.broadcast stream, data["data"]
33
+ end
34
+ end)
@@ -4,7 +4,10 @@ module AnyCable
4
4
  module Rails
5
5
  module Ext
6
6
  autoload :JWT, "anycable/rails/ext/jwt"
7
- autoload :SignedStreams, "anycable/rails/ext/signed_streams"
7
+
8
+ # These features are included by default
9
+ require "anycable/rails/ext/signed_streams"
10
+ require "anycable/rails/ext/whisper"
8
11
  end
9
12
  end
10
13
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+
5
+ ActionCable::Channel::Base.prepend(Module.new do
6
+ # Whispering support
7
+ def whispers_to(broadcasting)
8
+ return super unless anycabled?
9
+
10
+ connection.anycable_socket.whisper identifier, broadcasting
11
+ end
12
+
13
+ # Unsubscribing relies on the channel state (which is not persistent in AnyCable).
14
+ # Thus, we pretend that the stream is registered to make Action Cable do its unsubscribing job.
15
+ def stop_stream_from(broadcasting)
16
+ streams[broadcasting] = true if anycabled?
17
+ super
18
+ end
19
+
20
+ # For AnyCable, unsubscribing from all streams is a separate operation,
21
+ # so we use a special constant to indicate it.
22
+ def stop_all_streams
23
+ if anycabled?
24
+ streams.clear
25
+ streams[AnyCable::Rails::Server::PubSub::ALL_STREAMS] = true
26
+ end
27
+ super
28
+ end
29
+
30
+ # Make rejected status accessible from outside
31
+ def rejected? = subscription_rejected?
32
+
33
+ private
34
+
35
+ def anycabled? = connection.anycabled?
36
+ end)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+ require "anycable/rails/connections/serializable_identification"
5
+
6
+ ActionCable::Connection::Base.include(AnyCable::Rails::Connections::SerializableIdentification)
7
+ ActionCable::Connection::Base.prepend(Module.new do
8
+ def anycabled?
9
+ anycable_socket
10
+ end
11
+
12
+ # Allow overriding #subscriptions to use a custom implementation
13
+ attr_writer :subscriptions
14
+
15
+ # Alias for the #socket which is only set within AnyCable RPC context
16
+ attr_accessor :anycable_socket
17
+
18
+ # Enhance #send_welcome_message to include sid if present
19
+ def send_welcome_message
20
+ transmit({
21
+ type: ActionCable::INTERNAL[:message_types][:welcome],
22
+ sid: env["anycable.sid"]
23
+ }.compact)
24
+ end
25
+
26
+ def subscribe_to_internal_channel
27
+ super unless anycabled?
28
+ end
29
+ end)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anycable/rails/connections/session_proxy"
4
+
5
+ module AnyCable
6
+ module Rails
7
+ module Connections
8
+ module PersistentSession
9
+ def handle_open
10
+ super.tap { commit_session! }
11
+ end
12
+
13
+ def handle_channel_command(*)
14
+ super.tap { commit_session! }
15
+ end
16
+
17
+ def request
18
+ @request ||= super.tap do |req|
19
+ next unless socket.session
20
+ req.env[::Rack::RACK_SESSION] =
21
+ SessionProxy.new(req.env[::Rack::RACK_SESSION], socket.session)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def commit_session!
28
+ return unless defined?(@request) && request.session.respond_to?(:loaded?) && request.session.loaded?
29
+
30
+ socket.session = request.session.to_json
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ AnyCable::Rails::Connection.prepend(
38
+ AnyCable::Rails::Connections::PersistentSession
39
+ )
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+
5
+ module AnyCable
6
+ module Rails
7
+ class Current < ActiveSupport::CurrentAttributes
8
+ attribute :identifier
9
+ end
10
+
11
+ # Wrap ActionCable.server to provide a custom executor
12
+ # and a pubsub adapter
13
+ class Server < SimpleDelegator
14
+ # Implements an executor inteface
15
+ class Executor
16
+ class NoopTimer
17
+ def shutdown = nil
18
+ end
19
+
20
+ NOOP_TIMER = NoopTimer.new.freeze
21
+
22
+ def post(...)
23
+ raise NonImplementedError, "Executor#post is not implemented in AnyCable context"
24
+ end
25
+
26
+ def timer(...) = NOOP_TIMER
27
+ end
28
+
29
+ # A signleton executor for all connections
30
+ EXECUTOR = Executor.new.freeze
31
+
32
+ # PubSub adapter to manage streams configuration
33
+ # for the underlying socket
34
+ class PubSub
35
+ private attr_reader :socket
36
+
37
+ ALL_STREAMS = Data.define(:to_str).new("all")
38
+
39
+ def initialize(socket) = @socket = socket
40
+
41
+ def subscribe(channel, _message_callback, success_callback = nil)
42
+ socket.subscribe identifier, channel
43
+ success_callback&.call
44
+ end
45
+
46
+ def unsubscribe(channel, _message_callback)
47
+ if channel == ALL_STREAMS
48
+ socket.unsubscribe_from_all identifier
49
+ else
50
+ socket.unsubscribe identifier, channel
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def identifier = Current.identifier
57
+ end
58
+
59
+ attr_accessor :pubsub, :executor
60
+
61
+ def self.for(server, socket)
62
+ new(server).tap do |srv|
63
+ srv.executor = EXECUTOR
64
+ srv.pubsub = PubSub.new(socket)
65
+ end
66
+ end
67
+ end
68
+
69
+ class Connection
70
+ class Subscriptions < ::ActionCable::Connection::Subscriptions
71
+ # Wrap the original #execute_command to pre-initialize the channel for unsubscribe/message and
72
+ # return true/false to indicate successful/unsuccessful subscription.
73
+ def execute_command(data)
74
+ cmd = data["command"]
75
+
76
+ # We need the current channel identifier in pub/sub
77
+ Current.identifier = data["identifier"]
78
+
79
+ load(data["identifier"]) unless cmd == "subscribe"
80
+
81
+ super
82
+
83
+ return true unless cmd == "subscribe"
84
+
85
+ subscription = subscriptions[data["identifier"]]
86
+ !(subscription.nil? || subscription.rejected?)
87
+ end
88
+
89
+ # Restore channels from the list of identifiers and the state
90
+ def restore(subscriptions, istate)
91
+ subscriptions.each do |id|
92
+ channel = load(id)
93
+ channel.__istate__ = ActiveSupport::JSON.decode(istate[id]) if istate[id]
94
+ end
95
+ end
96
+
97
+ # Find or create a channel for a given identifier
98
+ def load(identifier)
99
+ return subscriptions[identifier] if subscriptions[identifier]
100
+
101
+ subscription = subscription_from_identifier(identifier)
102
+ raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}" unless subscription
103
+
104
+ subscriptions[identifier] = subscription
105
+ end
106
+ end
107
+
108
+ # We store logger tags in the connection state to be able
109
+ # to re-use them in the subsequent calls
110
+ LOG_TAGS_IDENTIFIER = "__ltags__"
111
+
112
+ attr_reader :socket, :server
113
+
114
+ delegate :identifiers_json, to: :conn
115
+ delegate :cstate, :istate, to: :socket
116
+
117
+ def initialize(connection_class, socket, identifiers: nil, subscriptions: nil, server: ::ActionCable.server)
118
+ server = Server.for(server, socket)
119
+
120
+ @socket = socket
121
+ @server = server
122
+ # TODO: Move protocol to socket.env as "anycable.protocol"
123
+ @protocol = "actioncable-v1-json"
124
+
125
+ logger_tags = fetch_logger_tags_from_state
126
+ @logger = ActionCable::Server::TaggedLoggerProxy.new(AnyCable.logger, tags: logger_tags)
127
+
128
+ @conn = connection_class.new(server, self)
129
+ conn.subscriptions = Subscriptions.new(conn)
130
+ conn.identifiers_json = identifiers
131
+ conn.anycable_socket = socket
132
+ conn.subscriptions.restore(subscriptions, socket.istate) if subscriptions
133
+ end
134
+
135
+ # == AnyCable RPC interface [BEGIN] ==
136
+ def handle_open
137
+ logger.info started_request_message if access_logs?
138
+
139
+ return close unless allow_request_origin?
140
+
141
+ conn.handle_open
142
+
143
+ # Commit log tags to the connection state
144
+ socket.cstate.write(LOG_TAGS_IDENTIFIER, logger.tags.to_json) unless logger.tags.empty?
145
+
146
+ socket.closed?
147
+ end
148
+
149
+ def handle_close
150
+ conn.handle_close
151
+ close
152
+ true
153
+ end
154
+
155
+ def handle_channel_command(identifier, command, data)
156
+ conn.handle_incoming({"command" => command, "identifier" => identifier, "data" => data})
157
+ end
158
+ # == AnyCable RPC interface [END] ==
159
+
160
+ # == Action Cable socket interface [BEGIN]
161
+ attr_reader :protocol, :logger
162
+
163
+ def request
164
+ @request ||= begin
165
+ env = socket.env
166
+ environment = ::Rails.application.env_config.merge(env) if defined?(::Rails.application) && ::Rails.application
167
+ AnyCable::Rails::Rack.app.call(environment) if environment
168
+
169
+ ActionDispatch::Request.new(environment || env)
170
+ end
171
+ end
172
+
173
+ delegate :env, to: :request
174
+
175
+ def transmit(data)
176
+ socket.transmit ActiveSupport::JSON.encode(data)
177
+ end
178
+
179
+ def close(...)
180
+ return if socket.closed?
181
+ logger.info finished_request_message if access_logs?
182
+ socket.close(...)
183
+ end
184
+
185
+ def perform_work(receiver, method_name, *args)
186
+ raise ArgumentError, "Performing work is not supported within AnyCable"
187
+ end
188
+ # == Action Cable socket interface [END]
189
+
190
+ private
191
+
192
+ attr_reader :conn
193
+
194
+ def fetch_logger_tags_from_state
195
+ socket.cstate.read(LOG_TAGS_IDENTIFIER).yield_self do |raw_tags|
196
+ next [] unless raw_tags
197
+ ActiveSupport::JSON.decode(raw_tags)
198
+ end
199
+ end
200
+
201
+ def started_request_message
202
+ format(
203
+ 'Started "%s"%s for %s at %s',
204
+ request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
205
+ )
206
+ end
207
+
208
+ def finished_request_message
209
+ format(
210
+ 'Finished "%s"%s for %s at %s',
211
+ request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
212
+ )
213
+ end
214
+
215
+ def allow_request_origin?
216
+ return true unless socket.env.key?("HTTP_ORIGIN")
217
+
218
+ server.allow_request_origin?(socket.env)
219
+ end
220
+
221
+ def access_logs?
222
+ AnyCable.config.access_logs_disabled == false
223
+ end
224
+ end
225
+ end
226
+ end
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "anycable/rails/action_cable_ext/connection"
4
- require "anycable/rails/action_cable_ext/channel"
5
- require "anycable/rails/action_cable_ext/remote_connections"
6
- require "anycable/rails/action_cable_ext/broadcast_options"
7
- require "anycable/rails/action_cable_ext/signed_streams"
8
-
9
3
  require "anycable/rails/channel_state"
10
4
  require "anycable/rails/connection_factory"
11
5
 
@@ -24,7 +24,7 @@ module RuboCop
24
24
  # end
25
25
  # end
26
26
  #
27
- class InstanceVars < RuboCop::Cop::Cop
27
+ class InstanceVars < RuboCop::Cop::Base
28
28
  MSG = "Channel instance variables are not supported in AnyCable. Use `state_attr_accessor` instead"
29
29
 
30
30
  def on_class(node)
@@ -13,7 +13,7 @@ module RuboCop
13
13
  # periodically(:do_something, every: 2.seconds)
14
14
  # end
15
15
  #
16
- class PeriodicalTimers < RuboCop::Cop::Cop
16
+ class PeriodicalTimers < RuboCop::Cop::Base
17
17
  MSG = "Periodical Timers are not supported in AnyCable"
18
18
 
19
19
  def_node_matcher :calls_periodically?, <<-PATTERN
@@ -34,7 +34,7 @@ module RuboCop
34
34
  # end
35
35
  # end
36
36
  #
37
- class StreamFrom < RuboCop::Cop::Cop
37
+ class StreamFrom < RuboCop::Cop::Base
38
38
  def_node_matcher :stream_from_with_block?, <<-PATTERN
39
39
  (block {(send _ :stream_from ...) (send _ :stream_for ...)} ...)
40
40
  PATTERN
@@ -81,16 +81,14 @@ module RuboCop
81
81
 
82
82
  def add_callback_offense(node)
83
83
  add_offense(
84
- node,
85
- location: :expression,
84
+ node.loc.expression,
86
85
  message: "Custom stream callbacks are not supported in AnyCable"
87
86
  )
88
87
  end
89
88
 
90
89
  def add_custom_coder_offense(node)
91
90
  add_offense(
92
- node,
93
- location: :expression,
91
+ node.loc.expression,
94
92
  message: "Custom coders are not supported in AnyCable"
95
93
  )
96
94
  end
@@ -0,0 +1,27 @@
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/periodical_timers"
8
+ require_relative "rubocop/cops/anycable/instance_vars"
9
+
10
+ module RuboCop
11
+ module AnyCable # :nodoc:
12
+ CONFIG_DEFAULT = Pathname.new(__dir__).join("rubocop", "config", "default.yml").freeze
13
+
14
+ # Merge anycable config into default configuration
15
+ # See https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
16
+ def self.inject!
17
+ path = CONFIG_DEFAULT.to_s
18
+ puts "configuration from #{path}" if ConfigLoader.debug?
19
+ hash = ConfigLoader.send(:load_yaml_configuration, path)
20
+ config = Config.new(hash, path)
21
+ config = ConfigLoader.merge_with_default(config, path)
22
+ ConfigLoader.instance_variable_set(:@default_configuration, config)
23
+ end
24
+ end
25
+ end
26
+
27
+ RuboCop::AnyCable.inject!
@@ -11,8 +11,8 @@ module AnyCable
11
11
 
12
12
  private
13
13
 
14
- def anycable_tracking_socket_id(&block)
15
- Rails.with_socket_id(request.headers[AnyCable.config.socket_id_header], &block)
14
+ def anycable_tracking_socket_id(&)
15
+ Rails.with_socket_id(request.headers[AnyCable.config.socket_id_header], &)
16
16
  end
17
17
  end
18
18
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AnyCable
4
4
  module Rails
5
- VERSION = "1.5.3"
5
+ VERSION = "1.5.5"
6
6
  end
7
7
  end
@@ -84,7 +84,7 @@ end
84
84
 
85
85
  # Warn if application has been already initialized.
86
86
  # AnyCable should be loaded before initialization in order to work correctly.
87
- if defined?(::Rails) && ::Rails.application && ::Rails.application.initialized?
87
+ if defined?(::Rails) && ::Rails.application&.initialized?
88
88
  puts("\n**************************************************")
89
89
  puts(
90
90
  "⛔️ WARNING: AnyCable loaded after application initialization. Might not work correctly.\n" \
@@ -70,6 +70,8 @@ module AnyCableRailsGenerators
70
70
  template "anycable.yml"
71
71
  end
72
72
 
73
+ template "anycable.toml"
74
+
73
75
  update_cable_yml
74
76
  end
75
77
 
@@ -168,6 +170,7 @@ module AnyCableRailsGenerators
168
170
  ANYCABLE_BROADCAST_ADAPTER: http
169
171
  ANYCABLE_RPC_HOST: anycable:50051
170
172
  ANYCABLE_DEBUG: ${ANYCABLE_DEBUG:-true}
173
+ ANYCABLE_SECRET: "anycable-local-secret"
171
174
 
172
175
  anycable:
173
176
  <<: *rails
@@ -208,6 +211,7 @@ module AnyCableRailsGenerators
208
211
  ANYCABLE_BROADCAST_ADAPTER: http
209
212
  ANYCABLE_RPC_HOST: http://rails:3000/_anycable
210
213
  ANYCABLE_DEBUG: ${ANYCABLE_DEBUG:-true}
214
+ ANYCABLE_SECRET: "anycable-local-secret"
211
215
  ─────────────────────────────────────────
212
216
  YML
213
217
  end
@@ -266,18 +270,11 @@ module AnyCableRailsGenerators
266
270
  end
267
271
  end
268
272
  unless contents.match?(/^ws:\s/)
269
- append_file file_name, "ws: bin/anycable-go #{anycable_go_options}", force: true
273
+ append_file file_name, "ws: bin/anycable-go", force: true
270
274
  end
271
275
  end
272
276
  end
273
277
 
274
- def anycable_go_options
275
- opts = ["--port=8080"]
276
- opts << "--broadcast_adapter=http" unless redis?
277
- opts << "--rpc_host=http://localhost:3000/_anycable" if http_rpc?
278
- opts.join(" ")
279
- end
280
-
281
278
  def file_exists?(name)
282
279
  in_root do
283
280
  return File.file?(name)
@@ -2,4 +2,4 @@ web: bin/rails s
2
2
  <%- unless http_rpc? -%>
3
3
  anycable: bundle exec anycable
4
4
  <%- end -%>
5
- ws: bin/anycable-go <%= anycable_go_options %>
5
+ ws: bin/anycable-go
@@ -0,0 +1,83 @@
1
+ # AnyCable server configuration (development).
2
+ #
3
+ # Read more at https://docs.anycable.io/anycable-go/configuration
4
+
5
+ # Public mode disables connection authentication, pub/sub streams and broadcasts verification
6
+ # public = false
7
+
8
+ # The application secret key
9
+ secret = "anycable-local-secret"
10
+
11
+ # Broadcasting adapters for app-to-clients messages
12
+ <%- if redis? -%>
13
+ broadcast_adapters = ["http", "redisx"]
14
+ <%- elsif nats? -%>
15
+ broadcast_adapters = ["http", "nats"]
16
+ <%- else -%>
17
+ broadcast_adapters = ["http"]
18
+ <%- end -%>
19
+
20
+ # Pub/sub adapter for inter-node communication
21
+ <%- if redis? -%>
22
+ pubsub_adapter = "redis"
23
+ <%- elsif nats? -%>
24
+ pubsub_adapter = "nats"
25
+ <%- else -%>
26
+ # pubsub_adapter = "redis" # or "nats"
27
+ <%- end -%>
28
+
29
+ [server]
30
+ host = "localhost"
31
+ port = 8080
32
+
33
+ [logging]
34
+ debug = true
35
+
36
+ # Read more about broker: https://docs.anycable.io/anycable-go/reliable_streams
37
+ [broker]
38
+ adapter = "memory"
39
+ history_ttl = 300
40
+ history_limit = 100
41
+ sessions_ttl = 300
42
+
43
+ [rpc]
44
+ <%- if http_rpc? -%>
45
+ host = "http://localhost:3000/_anycable"
46
+ <%- else -%>
47
+ host = "localhost:50051"
48
+ <%- end -%>
49
+ # Specify HTTP headers that must be proxied to the RPC service
50
+ proxy_headers = ["cookie"]
51
+ # RPC concurrency (max number of concurrent RPC requests)
52
+ concurrency = 28
53
+
54
+ # Read more about AnyCable JWT: https://docs.anycable.io/anycable-go/jwt_identification
55
+ [jwt]
56
+ # param = "jid"
57
+ # force = true
58
+
59
+ # Read more about AnyCable signed streams: https://docs.anycable.io/anycable-go/signed_streams
60
+ [streams]
61
+ # Enable public (unsigned) streams
62
+ # public = true
63
+ # Enable whispering support for pub/sub streams
64
+ # whisper = true
65
+ pubsub_channel = "$pubsub"
66
+ # turbo = true
67
+ # cable_ready = true
68
+
69
+ [redis]
70
+ <%- if redis? -%>
71
+ url = "redis://localhost:6379"
72
+ <%- else -%>
73
+ # url = "redis://localhost:6379"
74
+ <%- end -%>
75
+
76
+ <%- if nats? -%>
77
+ [nats]
78
+ servers = "nats://127.0.0.1:4222"
79
+ <%- end -%>
80
+
81
+ [http_broadcast]
82
+ port = 8090
83
+ path = "/_broadcast"
@@ -15,14 +15,15 @@ default: &default
15
15
  # Whether to enable gRPC level logging or not
16
16
  log_grpc: false
17
17
  <%- if redis? -%>
18
- # Use Redis to broadcast messages to AnyCable server
19
- broadcast_adapter: redis
18
+ # Use Redis Streams to broadcast messages to AnyCable server
19
+ broadcast_adapter: redisx
20
20
  <%- elsif nats? -%>
21
21
  # Use NATS to broadcast messages to AnyCable server
22
22
  broadcast_adapter: nats
23
23
  <%- else -%>
24
24
  # Use HTTP broadcaster
25
25
  broadcast_adapter: http
26
+ http_broadcast_url: "http://localhost:8090/_broadcast"
26
27
  <%- end -%>
27
28
  <%- if redis? -%>
28
29
  # You can use REDIS_URL env var to configure Redis URL.
@@ -37,6 +38,8 @@ default: &default
37
38
  # Read more about AnyCable RPC: <%= DOCS_ROOT %>/anycable-go/rpc
38
39
  http_rpc_mount_path: "/_anycable"
39
40
  <%- end -%>
41
+ # Must be the same as in your AnyCable server config
42
+ secret: "anycable-local-secret"
40
43
 
41
44
  development:
42
45
  <<: *default
@@ -51,3 +54,4 @@ test:
51
54
  production:
52
55
  <<: *default
53
56
  websocket_url: ~
57
+ secret: ~
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable-rails-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.3
4
+ version: 1.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-13 00:00:00.000000000 Z
11
+ date: 2024-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anycable-core
@@ -30,20 +30,20 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '6.0'
33
+ version: '7.0'
34
34
  - - "<"
35
35
  - !ruby/object:Gem::Version
36
- version: '8'
36
+ version: '9.0'
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
- version: '6.0'
43
+ version: '7.0'
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
- version: '8'
46
+ version: '9.0'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: globalid
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -77,14 +77,9 @@ files:
77
77
  - lib/anycable/rails/action_cable_ext/channel.rb
78
78
  - lib/anycable/rails/action_cable_ext/connection.rb
79
79
  - lib/anycable/rails/action_cable_ext/remote_connections.rb
80
- - lib/anycable/rails/action_cable_ext/signed_streams.rb
81
80
  - lib/anycable/rails/channel_state.rb
82
81
  - lib/anycable/rails/compatibility.rb
83
82
  - lib/anycable/rails/compatibility/rubocop.rb
84
- - lib/anycable/rails/compatibility/rubocop/config/default.yml
85
- - lib/anycable/rails/compatibility/rubocop/cops/anycable/instance_vars.rb
86
- - lib/anycable/rails/compatibility/rubocop/cops/anycable/periodical_timers.rb
87
- - lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb
88
83
  - lib/anycable/rails/config.rb
89
84
  - lib/anycable/rails/connection.rb
90
85
  - lib/anycable/rails/connection_factory.rb
@@ -93,13 +88,24 @@ files:
93
88
  - lib/anycable/rails/connections/session_proxy.rb
94
89
  - lib/anycable/rails/ext.rb
95
90
  - lib/anycable/rails/ext/jwt.rb
91
+ - lib/anycable/rails/ext/signed_streams.rb
92
+ - lib/anycable/rails/ext/whisper.rb
96
93
  - lib/anycable/rails/helper.rb
97
94
  - lib/anycable/rails/middlewares/executor.rb
98
95
  - lib/anycable/rails/middlewares/log_tagging.rb
96
+ - lib/anycable/rails/next/action_cable_ext/channel.rb
97
+ - lib/anycable/rails/next/action_cable_ext/connection.rb
98
+ - lib/anycable/rails/next/connection.rb
99
+ - lib/anycable/rails/next/connection/persistent_session.rb
99
100
  - lib/anycable/rails/object_serializer.rb
100
101
  - lib/anycable/rails/pubsub_channel.rb
101
102
  - lib/anycable/rails/rack.rb
102
103
  - lib/anycable/rails/railtie.rb
104
+ - lib/anycable/rails/rubocop.rb
105
+ - lib/anycable/rails/rubocop/config/default.yml
106
+ - lib/anycable/rails/rubocop/cops/anycable/instance_vars.rb
107
+ - lib/anycable/rails/rubocop/cops/anycable/periodical_timers.rb
108
+ - lib/anycable/rails/rubocop/cops/anycable/stream_from.rb
103
109
  - lib/anycable/rails/socket_id_tracking.rb
104
110
  - lib/anycable/rails/version.rb
105
111
  - lib/generators/anycable/bin/USAGE
@@ -110,6 +116,7 @@ files:
110
116
  - lib/generators/anycable/setup/USAGE
111
117
  - lib/generators/anycable/setup/setup_generator.rb
112
118
  - lib/generators/anycable/setup/templates/Procfile.dev.tt
119
+ - lib/generators/anycable/setup/templates/anycable.toml.tt
113
120
  - lib/generators/anycable/setup/templates/bin/anycable-go.tt
114
121
  - lib/generators/anycable/setup/templates/config/anycable.yml.tt
115
122
  - lib/generators/anycable/setup/templates/config/cable.yml.tt
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "action_cable"
4
-
5
- ActionCable::Connection::Base.include(Module.new do
6
- # This method is assumed to be overriden in the connection class to enable public
7
- # streams
8
- def allow_public_streams?
9
- false
10
- end
11
- end)
12
-
13
- # Handle $pubsub channel in Subscriptions
14
- ActionCable::Connection::Subscriptions.prepend(Module.new do
15
- # The contents are mostly copied from the original,
16
- # there is no good way to configure channels mapping due to #safe_constantize
17
- # and the layers of JSON
18
- # https://github.com/rails/rails/blob/main/actioncable/lib/action_cable/connection/subscriptions.rb
19
- def add(data)
20
- id_key = data["identifier"]
21
- id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
22
-
23
- return if subscriptions.key?(id_key)
24
-
25
- return super unless id_options[:channel] == "$pubsub"
26
-
27
- subscription = AnyCable::Rails::PubSubChannel.new(connection, id_key, id_options)
28
- subscriptions[id_key] = subscription
29
- subscription.subscribe_to_channel
30
- end
31
- end)