anycable-rails 1.2.1 → 1.3.0
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 +0 -1
- data/lib/anycable/rails/action_cable_ext/channel.rb +47 -0
- data/lib/anycable/rails/action_cable_ext/connection.rb +30 -0
- data/lib/anycable/rails/{actioncable → action_cable_ext}/remote_connections.rb +3 -1
- data/lib/anycable/rails/channel_state.rb +6 -6
- data/lib/anycable/rails/connection.rb +207 -0
- data/lib/anycable/rails/connection_factory.rb +44 -0
- data/lib/anycable/rails/connections/persistent_session.rb +40 -0
- data/lib/anycable/rails/connections/serializable_identification.rb +46 -0
- data/lib/anycable/rails/connections/session_proxy.rb +81 -0
- data/lib/anycable/rails/railtie.rb +8 -17
- data/lib/anycable/rails/version.rb +1 -1
- data/lib/anycable/rails.rb +11 -0
- metadata +10 -10
- data/lib/anycable/rails/actioncable/channel.rb +0 -40
- data/lib/anycable/rails/actioncable/connection/persistent_session.rb +0 -34
- data/lib/anycable/rails/actioncable/connection/serializable_identification.rb +0 -42
- data/lib/anycable/rails/actioncable/connection.rb +0 -220
- data/lib/anycable/rails/actioncable/testing.rb +0 -33
- data/lib/anycable/rails/refinements/subscriptions.rb +0 -20
- data/lib/anycable/rails/session_proxy.rb +0 -79
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 37be6474db2d3efc3493ec6fdf5b3826fbdfd84f9c4bc5f4574f1e0497457ec1
|
4
|
+
data.tar.gz: bafe87b642e1aaf15c5b09650ed271081d0ba53738507077ed1075b2de5e0ef2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b656f5a17009a19aacef7c1aac587299b5bdca24079e3e6a1ce2c9e652e26a7d65f08e34f0022e1ed4a9e42d8783af9d7d9e85a9059577ec224a523b7c12378e
|
7
|
+
data.tar.gz: 383c128d5404a7ed42bba43a426cb68bd10be23607557bce7a8ef46cd1ea1cf2777c918e44a4c71f307a0f973ff863aef36f547290ce821d65251e7f9a62a3fd
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 1.3.0 (2021-02-21)
|
6
|
+
|
7
|
+
- Introduce `AnyCable::Rails.extend_adapter!` to make any pubsub adapter AnyCable-compatible. ([@palkan][])
|
8
|
+
|
9
|
+
- Refactored Action Cable patching to preserve original functionality and avoid monkey-patching collisions. ([@palkan][])
|
10
|
+
|
5
11
|
## 1.2.1 (2021-01-31)
|
6
12
|
|
7
13
|
- Add a temporary fix to be compatible with `sentry-rails`. ([@palkan][])
|
data/README.md
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
[](https://rubygems.org/gems/anycable-rails)
|
2
2
|
[](https://github.com/anycable/anycable-rails/actions)
|
3
|
-
[](https://gitter.im/anycable/Lobby)
|
4
3
|
[](https://docs.anycable.io/rails/getting_started)
|
5
4
|
|
6
5
|
# AnyCable Rails
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable/channel"
|
4
|
+
|
5
|
+
ActionCable::Channel::Base.prepend(Module.new do
|
6
|
+
def subscribe_to_channel(force: false)
|
7
|
+
return if anycabled? && !force
|
8
|
+
super()
|
9
|
+
end
|
10
|
+
|
11
|
+
def handle_subscribe
|
12
|
+
subscribe_to_channel(force: true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def start_periodic_timers
|
16
|
+
super unless anycabled?
|
17
|
+
end
|
18
|
+
|
19
|
+
def stop_periodic_timers
|
20
|
+
super unless anycabled?
|
21
|
+
end
|
22
|
+
|
23
|
+
def stream_from(broadcasting, _callback = nil, **)
|
24
|
+
return super unless anycabled?
|
25
|
+
|
26
|
+
connection.anycable_socket.subscribe identifier, broadcasting
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop_stream_from(broadcasting)
|
30
|
+
return super unless anycabled?
|
31
|
+
|
32
|
+
connection.anycable_socket.unsubscribe identifier, broadcasting
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop_all_streams
|
36
|
+
return super unless anycabled?
|
37
|
+
|
38
|
+
connection.anycable_socket.unsubscribe_from_all identifier
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def anycabled?
|
44
|
+
# Use instance variable check here for testing compatibility
|
45
|
+
connection.instance_variable_defined?(:@anycable_socket)
|
46
|
+
end
|
47
|
+
end)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable/connection"
|
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
|
+
attr_reader :anycable_socket
|
9
|
+
attr_accessor :anycable_request_builder
|
10
|
+
|
11
|
+
# In AnyCable, we lazily populate env by passing it through the middleware chain,
|
12
|
+
# so we access it via #request
|
13
|
+
def env
|
14
|
+
return super unless anycabled?
|
15
|
+
|
16
|
+
request.env
|
17
|
+
end
|
18
|
+
|
19
|
+
def anycabled?
|
20
|
+
@anycable_socket
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def request
|
26
|
+
return super unless anycabled?
|
27
|
+
|
28
|
+
@request ||= anycable_request_builder.build_rack_request(@env)
|
29
|
+
end
|
30
|
+
end)
|
@@ -2,10 +2,12 @@
|
|
2
2
|
|
3
3
|
require "action_cable/remote_connections"
|
4
4
|
|
5
|
-
ActionCable::RemoteConnections::RemoteConnection.include(
|
5
|
+
ActionCable::RemoteConnections::RemoteConnection.include(AnyCable::Rails::Connections::SerializableIdentification)
|
6
6
|
|
7
7
|
ActionCable::RemoteConnections::RemoteConnection.prepend(Module.new do
|
8
8
|
def disconnect(reconnect: true)
|
9
|
+
# Legacy Action Cable functionality if case we're not fully migrated yet
|
10
|
+
super() unless AnyCable::Rails.enabled?
|
9
11
|
::AnyCable.broadcast_adapter.broadcast_command("disconnect", identifier: identifiers_json, reconnect: reconnect)
|
10
12
|
end
|
11
13
|
end)
|
@@ -10,11 +10,11 @@ module AnyCable
|
|
10
10
|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
11
11
|
def #{name}
|
12
12
|
return @#{name} if instance_variable_defined?(:@#{name})
|
13
|
-
@#{name} = AnyCable::Rails.deserialize(__istate__["#{name}"], json: true) if
|
13
|
+
@#{name} = AnyCable::Rails.deserialize(__istate__["#{name}"], json: true) if anycabled?
|
14
14
|
end
|
15
15
|
|
16
16
|
def #{name}=(val)
|
17
|
-
__istate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if
|
17
|
+
__istate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if anycabled?
|
18
18
|
instance_variable_set(:@#{name}, val)
|
19
19
|
end
|
20
20
|
RUBY
|
@@ -41,7 +41,7 @@ module AnyCable
|
|
41
41
|
attr_writer :__istate__
|
42
42
|
|
43
43
|
def __istate__
|
44
|
-
@__istate__ ||= connection.
|
44
|
+
@__istate__ ||= connection.anycable_socket.istate
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
@@ -53,11 +53,11 @@ module AnyCable
|
|
53
53
|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
54
54
|
def #{name}
|
55
55
|
return @#{name} if instance_variable_defined?(:@#{name})
|
56
|
-
@#{name} = AnyCable::Rails.deserialize(__cstate__["#{name}"], json: true) if
|
56
|
+
@#{name} = AnyCable::Rails.deserialize(__cstate__["#{name}"], json: true) if anycabled?
|
57
57
|
end
|
58
58
|
|
59
59
|
def #{name}=(val)
|
60
|
-
__cstate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if
|
60
|
+
__cstate__["#{name}"] = AnyCable::Rails.serialize(val, json: true) if anycabled?
|
61
61
|
instance_variable_set(:@#{name}, val)
|
62
62
|
end
|
63
63
|
RUBY
|
@@ -84,7 +84,7 @@ module AnyCable
|
|
84
84
|
attr_writer :__cstate__
|
85
85
|
|
86
86
|
def __cstate__
|
87
|
-
@__cstate__ ||=
|
87
|
+
@__cstate__ ||= anycable_socket.cstate
|
88
88
|
end
|
89
89
|
end
|
90
90
|
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable/connection"
|
4
|
+
require "action_cable/channel"
|
5
|
+
|
6
|
+
module AnyCable
|
7
|
+
module Rails
|
8
|
+
# Enhance Action Cable connection
|
9
|
+
using(Module.new do
|
10
|
+
refine ActionCable::Connection::Base do
|
11
|
+
attr_writer :env, :websocket, :logger, :coder,
|
12
|
+
:subscriptions, :serialized_ids, :cached_ids, :server,
|
13
|
+
:anycable_socket
|
14
|
+
|
15
|
+
# Using public :send_welcome_message causes stack level too deep 🤷🏻♂️
|
16
|
+
def public_send_welcome_message
|
17
|
+
send_welcome_message
|
18
|
+
end
|
19
|
+
|
20
|
+
def public_request
|
21
|
+
request
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
refine ActionCable::Channel::Base do
|
26
|
+
def rejected?
|
27
|
+
subscription_rejected?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
refine ActionCable::Connection::Subscriptions do
|
32
|
+
# Find or add a subscription to the list
|
33
|
+
def fetch(identifier)
|
34
|
+
add("identifier" => identifier) unless subscriptions[identifier]
|
35
|
+
|
36
|
+
unless subscriptions[identifier]
|
37
|
+
raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}"
|
38
|
+
end
|
39
|
+
|
40
|
+
subscriptions[identifier]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end)
|
44
|
+
|
45
|
+
class Connection
|
46
|
+
# We store logger tags in the connection state to be able
|
47
|
+
# to re-use them in the subsequent calls
|
48
|
+
LOG_TAGS_IDENTIFIER = "__ltags__"
|
49
|
+
|
50
|
+
delegate :identifiers_json, to: :conn
|
51
|
+
|
52
|
+
attr_reader :socket, :logger
|
53
|
+
|
54
|
+
def initialize(connection_class, socket, identifiers: nil, subscriptions: nil)
|
55
|
+
@socket = socket
|
56
|
+
|
57
|
+
logger_tags = fetch_logger_tags_from_state
|
58
|
+
@logger = ActionCable::Connection::TaggedLoggerProxy.new(AnyCable.logger, tags: logger_tags)
|
59
|
+
|
60
|
+
# Instead of calling #initialize,
|
61
|
+
# we allocate an instance and setup all the required components manually
|
62
|
+
@conn = connection_class.allocate
|
63
|
+
# Required to access config (for access origin checks)
|
64
|
+
conn.server = ActionCable.server
|
65
|
+
conn.logger = logger
|
66
|
+
conn.anycable_socket = conn.websocket = socket
|
67
|
+
conn.env = socket.env
|
68
|
+
conn.coder = ActiveSupport::JSON
|
69
|
+
conn.subscriptions = ActionCable::Connection::Subscriptions.new(conn)
|
70
|
+
conn.serialized_ids = {}
|
71
|
+
conn.serialized_ids = ActiveSupport::JSON.decode(identifiers) if identifiers
|
72
|
+
conn.cached_ids = {}
|
73
|
+
conn.anycable_request_builder = self
|
74
|
+
|
75
|
+
return unless subscriptions
|
76
|
+
|
77
|
+
# Pre-initialize channels (for disconnect)
|
78
|
+
subscriptions.each do |id|
|
79
|
+
channel = conn.subscriptions.fetch(id)
|
80
|
+
next unless socket.istate[id]
|
81
|
+
|
82
|
+
channel.__istate__ = ActiveSupport::JSON.decode(socket.istate[id])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def handle_open
|
87
|
+
logger.info started_request_message if access_logs?
|
88
|
+
|
89
|
+
verify_origin! || return
|
90
|
+
|
91
|
+
conn.connect if conn.respond_to?(:connect)
|
92
|
+
|
93
|
+
socket.cstate.write(LOG_TAGS_IDENTIFIER, logger.tags.to_json) unless logger.tags.empty?
|
94
|
+
|
95
|
+
conn.public_send_welcome_message
|
96
|
+
rescue ::ActionCable::Connection::Authorization::UnauthorizedError
|
97
|
+
reject_request(
|
98
|
+
ActionCable::INTERNAL[:disconnect_reasons]&.[](:unauthorized) || "unauthorized"
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
def handle_close
|
103
|
+
logger.info finished_request_message if access_logs?
|
104
|
+
|
105
|
+
conn.subscriptions.unsubscribe_from_all
|
106
|
+
conn.disconnect if conn.respond_to?(:disconnect)
|
107
|
+
true
|
108
|
+
end
|
109
|
+
|
110
|
+
def handle_channel_command(identifier, command, data)
|
111
|
+
# We cannot use subscriptions#execute_command here,
|
112
|
+
# since we MUST return true of false, depending on the status
|
113
|
+
# of execution
|
114
|
+
channel = conn.subscriptions.fetch(identifier)
|
115
|
+
case command
|
116
|
+
when "subscribe"
|
117
|
+
channel.handle_subscribe
|
118
|
+
!channel.rejected?
|
119
|
+
when "unsubscribe"
|
120
|
+
conn.subscriptions.remove_subscription(channel)
|
121
|
+
true
|
122
|
+
when "message"
|
123
|
+
channel.perform_action ActiveSupport::JSON.decode(data)
|
124
|
+
true
|
125
|
+
else
|
126
|
+
false
|
127
|
+
end
|
128
|
+
# Support rescue_from
|
129
|
+
# https://github.com/rails/rails/commit/d2571e560c62116f60429c933d0c41a0e249b58b
|
130
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
131
|
+
rescue_with_handler(e) || raise
|
132
|
+
false
|
133
|
+
end
|
134
|
+
|
135
|
+
def build_rack_request(env)
|
136
|
+
environment = ::Rails.application.env_config.merge(env) if defined?(::Rails.application) && ::Rails.application
|
137
|
+
AnyCable::Rails::Rack.app.call(environment) if environment
|
138
|
+
|
139
|
+
ActionDispatch::Request.new(environment || env)
|
140
|
+
end
|
141
|
+
|
142
|
+
def action_cable_connection
|
143
|
+
conn
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
attr_reader :conn
|
149
|
+
|
150
|
+
def reject_request(reason, reconnect = false)
|
151
|
+
logger.info finished_request_message("Rejected") if access_logs?
|
152
|
+
conn.close(
|
153
|
+
reason: reason,
|
154
|
+
reconnect: reconnect
|
155
|
+
)
|
156
|
+
end
|
157
|
+
|
158
|
+
def fetch_logger_tags_from_state
|
159
|
+
socket.cstate.read(LOG_TAGS_IDENTIFIER).yield_self do |raw_tags|
|
160
|
+
next [] unless raw_tags
|
161
|
+
ActiveSupport::JSON.decode(raw_tags)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def started_request_message
|
166
|
+
format(
|
167
|
+
'Started "%s"%s for %s at %s',
|
168
|
+
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
def finished_request_message(reason = "Closed")
|
173
|
+
format(
|
174
|
+
'Finished "%s"%s for %s at %s (%s)',
|
175
|
+
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s, reason
|
176
|
+
)
|
177
|
+
end
|
178
|
+
|
179
|
+
def verify_origin!
|
180
|
+
return true unless socket.env.key?("HTTP_ORIGIN")
|
181
|
+
|
182
|
+
return true if conn.send(:allow_request_origin?)
|
183
|
+
|
184
|
+
reject_request(
|
185
|
+
ActionCable::INTERNAL[:disconnect_reasons]&.[](:invalid_request) || "invalid_request"
|
186
|
+
)
|
187
|
+
false
|
188
|
+
end
|
189
|
+
|
190
|
+
def access_logs?
|
191
|
+
AnyCable.config.access_logs_disabled == false
|
192
|
+
end
|
193
|
+
|
194
|
+
def request
|
195
|
+
conn.public_request
|
196
|
+
end
|
197
|
+
|
198
|
+
def request_loaded?
|
199
|
+
conn.instance_variable_defined?(:@request)
|
200
|
+
end
|
201
|
+
|
202
|
+
def rescue_with_handler(e)
|
203
|
+
conn.rescue_with_handler(e) if conn.respond_to?(:rescue_with_handler)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable/rails/connection"
|
4
|
+
|
5
|
+
module AnyCable
|
6
|
+
module Rails
|
7
|
+
class ConnectionFactory
|
8
|
+
def initialize(&block)
|
9
|
+
@mappings = []
|
10
|
+
@use_router = false
|
11
|
+
instance_eval(&block) if block
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(socket, **options)
|
15
|
+
connection_class = use_router? ? resolve_connection_class(socket.env) :
|
16
|
+
ActionCable.server.config.connection_class.call
|
17
|
+
|
18
|
+
AnyCable::Rails::Connection.new(connection_class, socket, **options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def map(route, &block)
|
22
|
+
raise ArgumentError, "Block is required" unless block
|
23
|
+
|
24
|
+
@use_router = true
|
25
|
+
mappings << [route, block]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :mappings, :use_router
|
31
|
+
alias_method :use_router?, :use_router
|
32
|
+
|
33
|
+
def resolve_connection_class(env)
|
34
|
+
path = env["PATH_INFO"]
|
35
|
+
|
36
|
+
mappings.each do |(prefix, resolver)|
|
37
|
+
return resolver.call if path.starts_with?(prefix)
|
38
|
+
end
|
39
|
+
|
40
|
+
raise "No connection class found matching #{path}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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 build_rack_request(env)
|
18
|
+
return super unless socket.session
|
19
|
+
|
20
|
+
super.tap do |req|
|
21
|
+
req.env[::Rack::RACK_SESSION] =
|
22
|
+
SessionProxy.new(req.env[::Rack::RACK_SESSION], socket.session)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def commit_session!
|
29
|
+
return unless request_loaded? && request.session.respond_to?(:loaded?) && request.session.loaded?
|
30
|
+
|
31
|
+
socket.session = request.session.to_json
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
AnyCable::Rails::Connection.prepend(
|
39
|
+
AnyCable::Rails::Connections::PersistentSession
|
40
|
+
)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module Rails
|
5
|
+
module Connections
|
6
|
+
module SerializableIdentification
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def identified_by(*identifiers)
|
11
|
+
super
|
12
|
+
Array(identifiers).each do |identifier|
|
13
|
+
define_method(identifier) do
|
14
|
+
instance_variable_get(:"@#{identifier}") || fetch_identifier(identifier)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generate identifiers info.
|
21
|
+
# Converts GlobalID compatible vars to corresponding global IDs params.
|
22
|
+
def identifiers_hash
|
23
|
+
identifiers.each_with_object({}) do |id, acc|
|
24
|
+
obj = instance_variable_get("@#{id}")
|
25
|
+
next unless obj
|
26
|
+
|
27
|
+
acc[id] = AnyCable::Rails.serialize(obj)
|
28
|
+
end.compact
|
29
|
+
end
|
30
|
+
|
31
|
+
def identifiers_json
|
32
|
+
identifiers_hash.to_json
|
33
|
+
end
|
34
|
+
|
35
|
+
# Fetch identifier and deserialize if neccessary
|
36
|
+
def fetch_identifier(name)
|
37
|
+
return unless @cached_ids
|
38
|
+
|
39
|
+
@cached_ids[name] ||= @cached_ids.fetch(name) do
|
40
|
+
AnyCable::Rails.deserialize(@serialized_ids[name.to_s])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
module Rails
|
5
|
+
module Connections
|
6
|
+
# Wrap `request.session` to lazily load values provided
|
7
|
+
# in the RPC call (set by the previous calls)
|
8
|
+
class SessionProxy
|
9
|
+
attr_reader :rack_session, :socket_session
|
10
|
+
|
11
|
+
def initialize(rack_session, socket_session)
|
12
|
+
@rack_session = rack_session
|
13
|
+
@socket_session = JSON.parse(socket_session).with_indifferent_access
|
14
|
+
end
|
15
|
+
|
16
|
+
%i[has_key? [] []= fetch delete dig].each do |mid|
|
17
|
+
class_eval <<~CODE, __FILE__, __LINE__ + 1
|
18
|
+
def #{mid}(*args, **kwargs, &block)
|
19
|
+
restore_key! args.first
|
20
|
+
rack_session.#{mid}(*args, **kwargs, &block)
|
21
|
+
end
|
22
|
+
CODE
|
23
|
+
end
|
24
|
+
|
25
|
+
alias_method :include?, :has_key?
|
26
|
+
alias_method :key?, :has_key?
|
27
|
+
|
28
|
+
%i[update merge! to_hash].each do |mid|
|
29
|
+
class_eval <<~CODE, __FILE__, __LINE__ + 1
|
30
|
+
def #{mid}(*args, **kwargs, &block)
|
31
|
+
restore!
|
32
|
+
rack_session.#{mid}(*args, **kwargs, &block)
|
33
|
+
end
|
34
|
+
CODE
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :to_h, :to_hash
|
38
|
+
|
39
|
+
def keys
|
40
|
+
rack_session.keys + socket_session.keys
|
41
|
+
end
|
42
|
+
|
43
|
+
# Delegate both publuc and private methods to rack_session
|
44
|
+
def respond_to_missing?(name, include_private = false)
|
45
|
+
return false if name == :marshal_dump || name == :_dump
|
46
|
+
rack_session.respond_to?(name, include_private) || super
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_missing(method, *args, &block)
|
50
|
+
if rack_session.respond_to?(method, true)
|
51
|
+
rack_session.send(method, *args, &block)
|
52
|
+
else
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# This method is used by StimulusReflex to obtain `@by`
|
58
|
+
def instance_variable_get(name)
|
59
|
+
super || rack_session.instance_variable_get(name)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def restore!
|
65
|
+
socket_session.keys.each(&method(:restore_key!))
|
66
|
+
end
|
67
|
+
|
68
|
+
def restore_key!(key)
|
69
|
+
return unless socket_session.key?(key)
|
70
|
+
val = socket_session.delete(key)
|
71
|
+
rack_session[key] =
|
72
|
+
if val.is_a?(String)
|
73
|
+
GlobalID::Locator.locate(val) || val
|
74
|
+
else
|
75
|
+
val
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -1,6 +1,11 @@
|
|
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
|
+
|
3
7
|
require "anycable/rails/channel_state"
|
8
|
+
require "anycable/rails/connection_factory"
|
4
9
|
|
5
10
|
module AnyCable
|
6
11
|
module Rails
|
@@ -43,30 +48,16 @@ module AnyCable
|
|
43
48
|
|
44
49
|
initializer "anycable.connection_factory", after: "action_cable.set_configs" do |app|
|
45
50
|
ActiveSupport.on_load(:action_cable) do
|
46
|
-
# Add AnyCable patch method stub (we use it in ChannelState to distinguish between Action Cable and AnyCable)
|
47
|
-
# NOTE: Method could be already defined if patch was loaded manually
|
48
|
-
ActionCable::Connection::Base.attr_reader(:anycable_socket) unless ActionCable::Connection::Base.method_defined?(:anycable_socket)
|
49
|
-
|
50
51
|
app.config.to_prepare do
|
51
|
-
AnyCable.connection_factory =
|
52
|
+
AnyCable.connection_factory = AnyCable::Rails::ConnectionFactory.new
|
52
53
|
end
|
53
54
|
|
54
|
-
if AnyCable
|
55
|
-
require "anycable/rails/
|
56
|
-
if AnyCable.config.persistent_session_enabled
|
57
|
-
require "anycable/rails/actioncable/connection/persistent_session"
|
58
|
-
end
|
55
|
+
if AnyCable.config.persistent_session_enabled?
|
56
|
+
require "anycable/rails/connections/persistent_session"
|
59
57
|
end
|
60
58
|
end
|
61
59
|
end
|
62
60
|
|
63
|
-
# Temp hack to fix Sentry vs AnyCable incompatibility
|
64
|
-
# See https://github.com/anycable/anycable-rails/issues/165
|
65
|
-
initializer "anycable.sentry_hack", after: :"sentry.extend_action_cable" do
|
66
|
-
next unless defined?(::Sentry::Rails::ActionCableExtensions::Connection)
|
67
|
-
Sentry::Rails::ActionCableExtensions::Connection.send :public, :handle_open, :handle_close
|
68
|
-
end
|
69
|
-
|
70
61
|
# Since Rails 6.1
|
71
62
|
if respond_to?(:server)
|
72
63
|
server do
|
data/lib/anycable/rails.rb
CHANGED
@@ -48,6 +48,17 @@ module AnyCable
|
|
48
48
|
val
|
49
49
|
end
|
50
50
|
end
|
51
|
+
|
52
|
+
module Extension
|
53
|
+
def broadcast(channel, payload)
|
54
|
+
super
|
55
|
+
::AnyCable.broadcast(channel, payload)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def extend_adapter!(adapter)
|
60
|
+
adapter.extend(Extension)
|
61
|
+
end
|
51
62
|
end
|
52
63
|
end
|
53
64
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: anycable-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- palkan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: anycable
|
@@ -150,12 +150,9 @@ files:
|
|
150
150
|
- lib/action_cable/subscription_adapter/anycable.rb
|
151
151
|
- lib/anycable-rails.rb
|
152
152
|
- lib/anycable/rails.rb
|
153
|
-
- lib/anycable/rails/
|
154
|
-
- lib/anycable/rails/
|
155
|
-
- lib/anycable/rails/
|
156
|
-
- lib/anycable/rails/actioncable/connection/serializable_identification.rb
|
157
|
-
- lib/anycable/rails/actioncable/remote_connections.rb
|
158
|
-
- lib/anycable/rails/actioncable/testing.rb
|
153
|
+
- lib/anycable/rails/action_cable_ext/channel.rb
|
154
|
+
- lib/anycable/rails/action_cable_ext/connection.rb
|
155
|
+
- lib/anycable/rails/action_cable_ext/remote_connections.rb
|
159
156
|
- lib/anycable/rails/channel_state.rb
|
160
157
|
- lib/anycable/rails/compatibility.rb
|
161
158
|
- lib/anycable/rails/compatibility/rubocop.rb
|
@@ -164,12 +161,15 @@ files:
|
|
164
161
|
- lib/anycable/rails/compatibility/rubocop/cops/anycable/periodical_timers.rb
|
165
162
|
- lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb
|
166
163
|
- lib/anycable/rails/config.rb
|
164
|
+
- lib/anycable/rails/connection.rb
|
165
|
+
- lib/anycable/rails/connection_factory.rb
|
166
|
+
- lib/anycable/rails/connections/persistent_session.rb
|
167
|
+
- lib/anycable/rails/connections/serializable_identification.rb
|
168
|
+
- lib/anycable/rails/connections/session_proxy.rb
|
167
169
|
- lib/anycable/rails/middlewares/executor.rb
|
168
170
|
- lib/anycable/rails/middlewares/log_tagging.rb
|
169
171
|
- lib/anycable/rails/rack.rb
|
170
172
|
- lib/anycable/rails/railtie.rb
|
171
|
-
- lib/anycable/rails/refinements/subscriptions.rb
|
172
|
-
- lib/anycable/rails/session_proxy.rb
|
173
173
|
- lib/anycable/rails/version.rb
|
174
174
|
- lib/generators/anycable/download/USAGE
|
175
175
|
- lib/generators/anycable/download/download_generator.rb
|
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "action_cable/channel"
|
4
|
-
|
5
|
-
module ActionCable
|
6
|
-
module Channel
|
7
|
-
class Base # :nodoc:
|
8
|
-
alias_method :handle_subscribe, :subscribe_to_channel
|
9
|
-
|
10
|
-
public :handle_subscribe, :subscription_rejected?
|
11
|
-
|
12
|
-
# Action Cable calls this method from inside the Subscriptions#add
|
13
|
-
# method, which we use to initialize the channel instance.
|
14
|
-
# We don't want to invoke `subscribed` callbacks every time we do that.
|
15
|
-
def subscribe_to_channel
|
16
|
-
# noop
|
17
|
-
end
|
18
|
-
|
19
|
-
def start_periodic_timers
|
20
|
-
# noop
|
21
|
-
end
|
22
|
-
|
23
|
-
def stop_periodic_timers
|
24
|
-
# noop
|
25
|
-
end
|
26
|
-
|
27
|
-
def stream_from(broadcasting, _callback = nil, _options = {})
|
28
|
-
connection.socket.subscribe identifier, broadcasting
|
29
|
-
end
|
30
|
-
|
31
|
-
def stop_stream_from(broadcasting)
|
32
|
-
connection.socket.unsubscribe identifier, broadcasting
|
33
|
-
end
|
34
|
-
|
35
|
-
def stop_all_streams
|
36
|
-
connection.socket.unsubscribe_from_all identifier
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,34 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionCable
|
4
|
-
module Connection
|
5
|
-
module PersistentSession
|
6
|
-
def handle_open
|
7
|
-
super.tap { commit_session! }
|
8
|
-
end
|
9
|
-
|
10
|
-
def handle_channel_command(*)
|
11
|
-
super.tap { commit_session! }
|
12
|
-
end
|
13
|
-
|
14
|
-
def build_rack_request
|
15
|
-
return super unless socket.session
|
16
|
-
|
17
|
-
super.tap do |req|
|
18
|
-
req.env[::Rack::RACK_SESSION] =
|
19
|
-
AnyCable::Rails::SessionProxy.new(req.env[::Rack::RACK_SESSION], socket.session)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def commit_session!
|
24
|
-
return unless request_loaded? && request.session.respond_to?(:loaded?) && request.session.loaded?
|
25
|
-
|
26
|
-
socket.session = request.session.to_json
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
::ActionCable::Connection::Base.prepend(
|
33
|
-
::ActionCable::Connection::PersistentSession
|
34
|
-
)
|
@@ -1,42 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ActionCable
|
4
|
-
module Connection
|
5
|
-
module SerializableIdentification
|
6
|
-
extend ActiveSupport::Concern
|
7
|
-
|
8
|
-
class_methods do
|
9
|
-
def identified_by(*identifiers)
|
10
|
-
super
|
11
|
-
Array(identifiers).each do |identifier|
|
12
|
-
define_method(identifier) do
|
13
|
-
instance_variable_get(:"@#{identifier}") || fetch_identifier(identifier)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
# Generate identifiers info.
|
20
|
-
# Converts GlobalID compatible vars to corresponding global IDs params.
|
21
|
-
def identifiers_hash
|
22
|
-
identifiers.each_with_object({}) do |id, acc|
|
23
|
-
obj = instance_variable_get("@#{id}")
|
24
|
-
next unless obj
|
25
|
-
|
26
|
-
acc[id] = AnyCable::Rails.serialize(obj)
|
27
|
-
end.compact
|
28
|
-
end
|
29
|
-
|
30
|
-
def identifiers_json
|
31
|
-
identifiers_hash.to_json
|
32
|
-
end
|
33
|
-
|
34
|
-
# Fetch identifier and deserialize if neccessary
|
35
|
-
def fetch_identifier(name)
|
36
|
-
@cached_ids[name] ||= @cached_ids.fetch(name) do
|
37
|
-
AnyCable::Rails.deserialize(ids[name.to_s])
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
@@ -1,220 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "action_cable/connection"
|
4
|
-
require "anycable/rails/actioncable/connection/serializable_identification"
|
5
|
-
require "anycable/rails/refinements/subscriptions"
|
6
|
-
require "anycable/rails/actioncable/channel"
|
7
|
-
require "anycable/rails/actioncable/remote_connections"
|
8
|
-
require "anycable/rails/session_proxy"
|
9
|
-
|
10
|
-
module ActionCable
|
11
|
-
module Connection
|
12
|
-
class Base # :nodoc:
|
13
|
-
# We store logger tags in the connection state to be able
|
14
|
-
# to re-use them in the subsequent calls
|
15
|
-
LOG_TAGS_IDENTIFIER = "__ltags__"
|
16
|
-
|
17
|
-
using AnyCable::Refinements::Subscriptions
|
18
|
-
|
19
|
-
include SerializableIdentification
|
20
|
-
|
21
|
-
attr_reader :socket
|
22
|
-
|
23
|
-
alias_method :anycable_socket, :socket
|
24
|
-
|
25
|
-
delegate :env, :session, to: :request
|
26
|
-
|
27
|
-
class << self
|
28
|
-
def call(socket, **options)
|
29
|
-
new(socket, nil, **options)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def initialize(socket, env, identifiers: "{}", subscriptions: nil)
|
34
|
-
if env
|
35
|
-
# If env is set, then somehow we're in the context of Action Cable
|
36
|
-
# Return and print a warning in #process
|
37
|
-
@request = ActionDispatch::Request.new(env)
|
38
|
-
return
|
39
|
-
end
|
40
|
-
|
41
|
-
@ids = ActiveSupport::JSON.decode(identifiers)
|
42
|
-
|
43
|
-
@ltags = socket.cstate.read(LOG_TAGS_IDENTIFIER).yield_self do |raw_tags|
|
44
|
-
next unless raw_tags
|
45
|
-
ActiveSupport::JSON.decode(raw_tags)
|
46
|
-
end
|
47
|
-
|
48
|
-
@cached_ids = {}
|
49
|
-
@coder = ActiveSupport::JSON
|
50
|
-
@socket = socket
|
51
|
-
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
|
52
|
-
|
53
|
-
return unless subscriptions
|
54
|
-
|
55
|
-
# Initialize channels (for disconnect)
|
56
|
-
subscriptions.each do |id|
|
57
|
-
channel = @subscriptions.fetch(id)
|
58
|
-
next unless socket.istate[id]
|
59
|
-
|
60
|
-
channel.__istate__ = ActiveSupport::JSON.decode(socket.istate[id])
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def process
|
65
|
-
# Use Rails logger here to print to stdout in development
|
66
|
-
logger.error invalid_request_message
|
67
|
-
logger.info finished_request_message
|
68
|
-
[404, {"Content-Type" => "text/plain"}, ["Page not found"]]
|
69
|
-
end
|
70
|
-
|
71
|
-
def invalid_request_message
|
72
|
-
"You're trying to connect to Action Cable server while using AnyCable. " \
|
73
|
-
"See https://docs.anycable.io/troubleshooting?id=server-raises-an-argumenterror-exception-when-client-tries-to-connect"
|
74
|
-
end
|
75
|
-
|
76
|
-
def handle_open
|
77
|
-
logger.info started_request_message if access_logs?
|
78
|
-
|
79
|
-
verify_origin! || return
|
80
|
-
|
81
|
-
connect if respond_to?(:connect)
|
82
|
-
|
83
|
-
socket.cstate.write(LOG_TAGS_IDENTIFIER, fetch_ltags.to_json)
|
84
|
-
|
85
|
-
send_welcome_message
|
86
|
-
rescue ActionCable::Connection::Authorization::UnauthorizedError
|
87
|
-
reject_request(
|
88
|
-
ActionCable::INTERNAL[:disconnect_reasons]&.[](:unauthorized) || "unauthorized"
|
89
|
-
)
|
90
|
-
end
|
91
|
-
|
92
|
-
def handle_close
|
93
|
-
logger.info finished_request_message if access_logs?
|
94
|
-
|
95
|
-
subscriptions.unsubscribe_from_all
|
96
|
-
disconnect if respond_to?(:disconnect)
|
97
|
-
true
|
98
|
-
end
|
99
|
-
|
100
|
-
def handle_channel_command(identifier, command, data)
|
101
|
-
channel = subscriptions.fetch(identifier)
|
102
|
-
case command
|
103
|
-
when "subscribe"
|
104
|
-
channel.handle_subscribe
|
105
|
-
!channel.subscription_rejected?
|
106
|
-
when "unsubscribe"
|
107
|
-
subscriptions.remove_subscription(channel)
|
108
|
-
true
|
109
|
-
when "message"
|
110
|
-
channel.perform_action ActiveSupport::JSON.decode(data)
|
111
|
-
true
|
112
|
-
else
|
113
|
-
false
|
114
|
-
end
|
115
|
-
end
|
116
|
-
# rubocop:enable Metrics/MethodLength
|
117
|
-
|
118
|
-
def close(reason: nil, reconnect: nil)
|
119
|
-
transmit(
|
120
|
-
type: ActionCable::INTERNAL[:message_types].fetch(:disconnect, "disconnect"),
|
121
|
-
reason: reason,
|
122
|
-
reconnect: reconnect
|
123
|
-
)
|
124
|
-
socket.close
|
125
|
-
end
|
126
|
-
|
127
|
-
def transmit(cable_message)
|
128
|
-
socket.transmit encode(cable_message)
|
129
|
-
end
|
130
|
-
|
131
|
-
def logger
|
132
|
-
@logger ||= TaggedLoggerProxy.new(AnyCable.logger, tags: ltags || [])
|
133
|
-
end
|
134
|
-
|
135
|
-
def request
|
136
|
-
@request ||= build_rack_request
|
137
|
-
end
|
138
|
-
|
139
|
-
private
|
140
|
-
|
141
|
-
attr_reader :ids, :ltags
|
142
|
-
|
143
|
-
def started_request_message
|
144
|
-
format(
|
145
|
-
'Started "%s"%s for %s at %s',
|
146
|
-
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
|
147
|
-
)
|
148
|
-
end
|
149
|
-
|
150
|
-
def finished_request_message(reason = "Closed")
|
151
|
-
format(
|
152
|
-
'Finished "%s"%s for %s at %s (%s)',
|
153
|
-
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s, reason
|
154
|
-
)
|
155
|
-
end
|
156
|
-
|
157
|
-
def access_logs?
|
158
|
-
AnyCable.config.access_logs_disabled == false
|
159
|
-
end
|
160
|
-
|
161
|
-
def fetch_ltags
|
162
|
-
if instance_variable_defined?(:@logger)
|
163
|
-
logger.tags
|
164
|
-
else
|
165
|
-
ltags
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def server
|
170
|
-
ActionCable.server
|
171
|
-
end
|
172
|
-
|
173
|
-
def verify_origin!
|
174
|
-
return true unless socket.env.key?("HTTP_ORIGIN")
|
175
|
-
|
176
|
-
return true if allow_request_origin?
|
177
|
-
|
178
|
-
reject_request(
|
179
|
-
ActionCable::INTERNAL[:disconnect_reasons]&.[](:invalid_request) || "invalid_request"
|
180
|
-
)
|
181
|
-
false
|
182
|
-
end
|
183
|
-
|
184
|
-
def reject_request(reason, reconnect = false)
|
185
|
-
logger.info finished_request_message("Rejected") if access_logs?
|
186
|
-
close(
|
187
|
-
reason: reason,
|
188
|
-
reconnect: reconnect
|
189
|
-
)
|
190
|
-
end
|
191
|
-
|
192
|
-
def build_rack_request
|
193
|
-
environment = Rails.application.env_config.merge(socket.env)
|
194
|
-
AnyCable::Rails::Rack.app.call(environment)
|
195
|
-
|
196
|
-
ActionDispatch::Request.new(environment)
|
197
|
-
end
|
198
|
-
|
199
|
-
def request_loaded?
|
200
|
-
instance_variable_defined?(:@request)
|
201
|
-
end
|
202
|
-
end
|
203
|
-
# rubocop:enable Metrics/ClassLength
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
# Support rescue_from
|
208
|
-
# https://github.com/rails/rails/commit/d2571e560c62116f60429c933d0c41a0e249b58b
|
209
|
-
if ActionCable::Connection::Base.respond_to?(:rescue_from)
|
210
|
-
ActionCable::Connection::Base.prepend(Module.new do
|
211
|
-
def handle_channel_command(*)
|
212
|
-
super
|
213
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
214
|
-
rescue_with_handler(e) || raise
|
215
|
-
false
|
216
|
-
end
|
217
|
-
end)
|
218
|
-
end
|
219
|
-
|
220
|
-
require "anycable/rails/actioncable/testing" if ::Rails.env.test?
|
@@ -1,33 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# This file contains patches to Action Cable testing modules
|
4
|
-
|
5
|
-
# Trigger autoload (if constant is defined)
|
6
|
-
begin
|
7
|
-
ActionCable::Channel::TestCase # rubocop:disable Lint/Void
|
8
|
-
ActionCable::Connection::TestCase
|
9
|
-
rescue NameError
|
10
|
-
return
|
11
|
-
end
|
12
|
-
|
13
|
-
ActionCable::Channel::ChannelStub.prepend(Module.new do
|
14
|
-
def subscribe_to_channel
|
15
|
-
handle_subscribe
|
16
|
-
end
|
17
|
-
end)
|
18
|
-
|
19
|
-
ActionCable::Channel::ConnectionStub.prepend(Module.new do
|
20
|
-
def socket
|
21
|
-
@socket ||= AnyCable::Socket.new(env: AnyCable::Env.new(url: "http://test.host", headers: {}))
|
22
|
-
end
|
23
|
-
|
24
|
-
alias_method :anycable_socket, :socket
|
25
|
-
end)
|
26
|
-
|
27
|
-
ActionCable::Connection::TestConnection.prepend(Module.new do
|
28
|
-
def initialize(request)
|
29
|
-
@request = request
|
30
|
-
@cached_ids = {}
|
31
|
-
super
|
32
|
-
end
|
33
|
-
end)
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AnyCable
|
4
|
-
module Refinements
|
5
|
-
module Subscriptions # :nodoc:
|
6
|
-
refine ActionCable::Connection::Subscriptions do
|
7
|
-
# Find or add a subscription to the list
|
8
|
-
def fetch(identifier)
|
9
|
-
add("identifier" => identifier) unless subscriptions[identifier]
|
10
|
-
|
11
|
-
unless subscriptions[identifier]
|
12
|
-
raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}"
|
13
|
-
end
|
14
|
-
|
15
|
-
subscriptions[identifier]
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,79 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AnyCable
|
4
|
-
module Rails
|
5
|
-
# Wrap `request.session` to lazily load values provided
|
6
|
-
# in the RPC call (set by the previous calls)
|
7
|
-
class SessionProxy
|
8
|
-
attr_reader :rack_session, :socket_session
|
9
|
-
|
10
|
-
def initialize(rack_session, socket_session)
|
11
|
-
@rack_session = rack_session
|
12
|
-
@socket_session = JSON.parse(socket_session).with_indifferent_access
|
13
|
-
end
|
14
|
-
|
15
|
-
%i[has_key? [] []= fetch delete dig].each do |mid|
|
16
|
-
class_eval <<~CODE, __FILE__, __LINE__ + 1
|
17
|
-
def #{mid}(*args, **kwargs, &block)
|
18
|
-
restore_key! args.first
|
19
|
-
rack_session.#{mid}(*args, **kwargs, &block)
|
20
|
-
end
|
21
|
-
CODE
|
22
|
-
end
|
23
|
-
|
24
|
-
alias_method :include?, :has_key?
|
25
|
-
alias_method :key?, :has_key?
|
26
|
-
|
27
|
-
%i[update merge! to_hash].each do |mid|
|
28
|
-
class_eval <<~CODE, __FILE__, __LINE__ + 1
|
29
|
-
def #{mid}(*args, **kwargs, &block)
|
30
|
-
restore!
|
31
|
-
rack_session.#{mid}(*args, **kwargs, &block)
|
32
|
-
end
|
33
|
-
CODE
|
34
|
-
end
|
35
|
-
|
36
|
-
alias_method :to_h, :to_hash
|
37
|
-
|
38
|
-
def keys
|
39
|
-
rack_session.keys + socket_session.keys
|
40
|
-
end
|
41
|
-
|
42
|
-
# Delegate both publuc and private methods to rack_session
|
43
|
-
def respond_to_missing?(name, include_private = false)
|
44
|
-
return false if name == :marshal_dump || name == :_dump
|
45
|
-
rack_session.respond_to?(name, include_private) || super
|
46
|
-
end
|
47
|
-
|
48
|
-
def method_missing(method, *args, &block)
|
49
|
-
if rack_session.respond_to?(method, true)
|
50
|
-
rack_session.send(method, *args, &block)
|
51
|
-
else
|
52
|
-
super
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
# This method is used by StimulusReflex to obtain `@by`
|
57
|
-
def instance_variable_get(name)
|
58
|
-
super || rack_session.instance_variable_get(name)
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
def restore!
|
64
|
-
socket_session.keys.each(&method(:restore_key!))
|
65
|
-
end
|
66
|
-
|
67
|
-
def restore_key!(key)
|
68
|
-
return unless socket_session.key?(key)
|
69
|
-
val = socket_session.delete(key)
|
70
|
-
rack_session[key] =
|
71
|
-
if val.is_a?(String)
|
72
|
-
GlobalID::Locator.locate(val) || val
|
73
|
-
else
|
74
|
-
val
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|