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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +4 -8
- data/lib/anycable/rails/action_cable_ext/channel.rb +20 -0
- data/lib/anycable/rails/action_cable_ext/remote_connections.rb +4 -14
- data/lib/anycable/rails/compatibility.rb +1 -0
- data/lib/anycable/rails/connection_factory.rb +20 -1
- data/lib/anycable/rails/connections/persistent_session.rb +5 -0
- data/lib/anycable/rails/connections/serializable_identification.rb +5 -0
- data/lib/anycable/rails/connections/session_proxy.rb +2 -2
- data/lib/anycable/rails/ext/jwt.rb +1 -1
- data/lib/anycable/rails/ext/signed_streams.rb +21 -0
- data/lib/anycable/rails/ext/whisper.rb +34 -0
- data/lib/anycable/rails/ext.rb +4 -1
- data/lib/anycable/rails/next/action_cable_ext/channel.rb +36 -0
- data/lib/anycable/rails/next/action_cable_ext/connection.rb +29 -0
- data/lib/anycable/rails/next/connection/persistent_session.rb +39 -0
- data/lib/anycable/rails/next/connection.rb +226 -0
- data/lib/anycable/rails/railtie.rb +0 -6
- data/lib/anycable/rails/socket_id_tracking.rb +2 -2
- data/lib/anycable/rails/version.rb +1 -1
- data/lib/generators/anycable/setup/setup_generator.rb +5 -8
- data/lib/generators/anycable/setup/templates/Procfile.dev.tt +1 -1
- data/lib/generators/anycable/setup/templates/anycable.toml.tt +83 -0
- data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +6 -2
- metadata +13 -7
- data/lib/anycable/rails/action_cable_ext/signed_streams.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ceb26b8e5c09c7bd5500007a29a79a2933cc38848a63da16af272a4a5d11771
|
4
|
+
data.tar.gz: 6db49c5ceca2e5b020fb7b225ebe2e1db63df8beb9858224502883ff35392529
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 >=
|
25
|
-
- Rails >= 6.0
|
26
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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,6 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
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
|
@@ -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,
|
49
|
+
def method_missing(method, ...)
|
50
50
|
if rack_session.respond_to?(method, true)
|
51
|
-
rack_session.send(method,
|
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)
|
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)
|
data/lib/anycable/rails/ext.rb
CHANGED
@@ -4,7 +4,10 @@ module AnyCable
|
|
4
4
|
module Rails
|
5
5
|
module Ext
|
6
6
|
autoload :JWT, "anycable/rails/ext/jwt"
|
7
|
-
|
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(&
|
15
|
-
Rails.with_socket_id(request.headers[AnyCable.config.socket_id_header], &
|
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
|
|
@@ -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
|
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)
|
@@ -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:
|
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.
|
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
|
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: '
|
33
|
+
version: '7.0'
|
34
34
|
- - "<"
|
35
35
|
- !ruby/object:Gem::Version
|
36
|
-
version: '
|
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: '
|
43
|
+
version: '7.0'
|
44
44
|
- - "<"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: '
|
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)
|