anycable-rails-core 1.5.3 → 1.5.4
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|