anycable-rails-core 1.5.3 → 1.5.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae33f5aaf1057a1aa63e16280048477ed7f6be5ff403e7a45bd4198aed3ef226
4
- data.tar.gz: dfedd48dbeddaf2e722093bec52f459c576c59a3c549e26afe00753ee770496b
3
+ metadata.gz: 6ceb26b8e5c09c7bd5500007a29a79a2933cc38848a63da16af272a4a5d11771
4
+ data.tar.gz: 6db49c5ceca2e5b020fb7b225ebe2e1db63df8beb9858224502883ff35392529
5
5
  SHA512:
6
- metadata.gz: 3e84c51f6fb7b2c7e7a1b1535b2f99b4db0d83ee3235b45104aad07e353924bf2ce91269b4dbb9b992f356a622594f3c2e44941edf60b47be899196ee8adfd83
7
- data.tar.gz: 6b9b15397056c29d3cbf01b566ea4282e41dc2065f8af827bb73f102c47e78d429aecbfd11d687660eb4c820225fa3411d9bd7bd4fb39a75155568a0e4e24e05
6
+ metadata.gz: d78c540124cec98c20e2004f49324580300a9be00eea194fa09200a8b30090b3448e3411dcba1a4002efa857997e0ed66da44a808efd17ceede3ce45fd97b751
7
+ data.tar.gz: 4e7916ccc0f8c18ed305eab329575c1bc840be9eeeb1534d4d3c661067a5f9a96ac761af3e8f2f9871a0f3bcbc7d30816e09a3e9998d7bffdf3555d36a88f515
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.5.4 (2024-10-08)
6
+
7
+ - Add [actioncable-next](https://github.com/anycable/actioncable-next) support. ([@palkan][])
8
+
9
+ - Generate `anycable.toml` in `anycable:setup` generator. ([@palkan][])
10
+
5
11
  ## 1.5.3 (2024-09-12)
6
12
 
7
13
  - 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)
@@ -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
 
@@ -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.4"
6
6
  end
7
7
  end
@@ -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/_anycable"
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.4
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-10-09 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,7 +77,6 @@ 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
@@ -93,9 +92,15 @@ files:
93
92
  - lib/anycable/rails/connections/session_proxy.rb
94
93
  - lib/anycable/rails/ext.rb
95
94
  - lib/anycable/rails/ext/jwt.rb
95
+ - lib/anycable/rails/ext/signed_streams.rb
96
+ - lib/anycable/rails/ext/whisper.rb
96
97
  - lib/anycable/rails/helper.rb
97
98
  - lib/anycable/rails/middlewares/executor.rb
98
99
  - lib/anycable/rails/middlewares/log_tagging.rb
100
+ - lib/anycable/rails/next/action_cable_ext/channel.rb
101
+ - lib/anycable/rails/next/action_cable_ext/connection.rb
102
+ - lib/anycable/rails/next/connection.rb
103
+ - lib/anycable/rails/next/connection/persistent_session.rb
99
104
  - lib/anycable/rails/object_serializer.rb
100
105
  - lib/anycable/rails/pubsub_channel.rb
101
106
  - lib/anycable/rails/rack.rb
@@ -110,6 +115,7 @@ files:
110
115
  - lib/generators/anycable/setup/USAGE
111
116
  - lib/generators/anycable/setup/setup_generator.rb
112
117
  - lib/generators/anycable/setup/templates/Procfile.dev.tt
118
+ - lib/generators/anycable/setup/templates/anycable.toml.tt
113
119
  - lib/generators/anycable/setup/templates/bin/anycable-go.tt
114
120
  - lib/generators/anycable/setup/templates/config/anycable.yml.tt
115
121
  - 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)