anycable-rails 1.0.7 → 1.3.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 +82 -0
- data/MIT-LICENSE +1 -1
- data/README.md +11 -12
- data/lib/anycable/rails/action_cable_ext/channel.rb +49 -0
- data/lib/anycable/rails/action_cable_ext/connection.rb +90 -0
- data/lib/anycable/rails/{actioncable → action_cable_ext}/remote_connections.rb +3 -1
- data/lib/anycable/rails/channel_state.rb +60 -5
- data/lib/anycable/rails/config.rb +6 -1
- data/lib/anycable/rails/connection.rb +211 -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/middlewares/log_tagging.rb +2 -2
- data/lib/anycable/rails/railtie.rb +22 -13
- data/lib/anycable/rails/version.rb +1 -1
- data/lib/anycable/rails.rb +11 -0
- data/lib/generators/anycable/setup/setup_generator.rb +21 -5
- data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +1 -1
- metadata +22 -35
- 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 -222
- 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
@@ -1,222 +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
|
-
# rubocop: disable Metrics/ClassLength
|
13
|
-
class Base # :nodoc:
|
14
|
-
# We store logger tags in the connection state to be able
|
15
|
-
# to re-use them in the subsequent calls
|
16
|
-
LOG_TAGS_IDENTIFIER = "__ltags__"
|
17
|
-
|
18
|
-
using AnyCable::Refinements::Subscriptions
|
19
|
-
|
20
|
-
include SerializableIdentification
|
21
|
-
|
22
|
-
attr_reader :socket
|
23
|
-
|
24
|
-
alias_method :anycable_socket, :socket
|
25
|
-
|
26
|
-
delegate :env, :session, to: :request
|
27
|
-
|
28
|
-
class << self
|
29
|
-
def call(socket, **options)
|
30
|
-
new(socket, nil, **options)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def initialize(socket, env, identifiers: "{}", subscriptions: nil)
|
35
|
-
if env
|
36
|
-
# If env is set, then somehow we're in the context of Action Cable
|
37
|
-
# Return and print a warning in #process
|
38
|
-
@request = ActionDispatch::Request.new(env)
|
39
|
-
return
|
40
|
-
end
|
41
|
-
|
42
|
-
@ids = ActiveSupport::JSON.decode(identifiers)
|
43
|
-
|
44
|
-
@ltags = socket.cstate.read(LOG_TAGS_IDENTIFIER).yield_self do |raw_tags|
|
45
|
-
next unless raw_tags
|
46
|
-
ActiveSupport::JSON.decode(raw_tags)
|
47
|
-
end
|
48
|
-
|
49
|
-
@cached_ids = {}
|
50
|
-
@coder = ActiveSupport::JSON
|
51
|
-
@socket = socket
|
52
|
-
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
|
53
|
-
|
54
|
-
return unless subscriptions
|
55
|
-
|
56
|
-
# Initialize channels (for disconnect)
|
57
|
-
subscriptions.each do |id|
|
58
|
-
channel = @subscriptions.fetch(id)
|
59
|
-
next unless socket.istate[id]
|
60
|
-
|
61
|
-
channel.__istate__ = ActiveSupport::JSON.decode(socket.istate[id])
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def process
|
66
|
-
# Use Rails logger here to print to stdout in development
|
67
|
-
logger.error invalid_request_message
|
68
|
-
logger.info finished_request_message
|
69
|
-
[404, {"Content-Type" => "text/plain"}, ["Page not found"]]
|
70
|
-
end
|
71
|
-
|
72
|
-
def invalid_request_message
|
73
|
-
"You're trying to connect to Action Cable server while using AnyCable. " \
|
74
|
-
"See https://docs.anycable.io/#/troubleshooting?id=server-raises-an-argumenterror-exception-when-client-tries-to-connect"
|
75
|
-
end
|
76
|
-
|
77
|
-
def handle_open
|
78
|
-
logger.info started_request_message if access_logs?
|
79
|
-
|
80
|
-
verify_origin! || return
|
81
|
-
|
82
|
-
connect if respond_to?(:connect)
|
83
|
-
|
84
|
-
socket.cstate.write(LOG_TAGS_IDENTIFIER, fetch_ltags.to_json)
|
85
|
-
|
86
|
-
send_welcome_message
|
87
|
-
rescue ActionCable::Connection::Authorization::UnauthorizedError
|
88
|
-
reject_request(
|
89
|
-
ActionCable::INTERNAL[:disconnect_reasons]&.[](:unauthorized) || "unauthorized"
|
90
|
-
)
|
91
|
-
end
|
92
|
-
|
93
|
-
def handle_close
|
94
|
-
logger.info finished_request_message if access_logs?
|
95
|
-
|
96
|
-
subscriptions.unsubscribe_from_all
|
97
|
-
disconnect if respond_to?(:disconnect)
|
98
|
-
true
|
99
|
-
end
|
100
|
-
|
101
|
-
# rubocop:disable Metrics/MethodLength
|
102
|
-
def handle_channel_command(identifier, command, data)
|
103
|
-
channel = subscriptions.fetch(identifier)
|
104
|
-
case command
|
105
|
-
when "subscribe"
|
106
|
-
channel.handle_subscribe
|
107
|
-
!channel.subscription_rejected?
|
108
|
-
when "unsubscribe"
|
109
|
-
subscriptions.remove_subscription(channel)
|
110
|
-
true
|
111
|
-
when "message"
|
112
|
-
channel.perform_action ActiveSupport::JSON.decode(data)
|
113
|
-
true
|
114
|
-
else
|
115
|
-
false
|
116
|
-
end
|
117
|
-
end
|
118
|
-
# rubocop:enable Metrics/MethodLength
|
119
|
-
|
120
|
-
def close(reason: nil, reconnect: nil)
|
121
|
-
transmit(
|
122
|
-
type: ActionCable::INTERNAL[:message_types].fetch(:disconnect, "disconnect"),
|
123
|
-
reason: reason,
|
124
|
-
reconnect: reconnect
|
125
|
-
)
|
126
|
-
socket.close
|
127
|
-
end
|
128
|
-
|
129
|
-
def transmit(cable_message)
|
130
|
-
socket.transmit encode(cable_message)
|
131
|
-
end
|
132
|
-
|
133
|
-
def logger
|
134
|
-
@logger ||= TaggedLoggerProxy.new(AnyCable.logger, tags: ltags || [])
|
135
|
-
end
|
136
|
-
|
137
|
-
def request
|
138
|
-
@request ||= build_rack_request
|
139
|
-
end
|
140
|
-
|
141
|
-
private
|
142
|
-
|
143
|
-
attr_reader :ids, :ltags
|
144
|
-
|
145
|
-
def started_request_message
|
146
|
-
format(
|
147
|
-
'Started "%s"%s for %s at %s',
|
148
|
-
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
|
149
|
-
)
|
150
|
-
end
|
151
|
-
|
152
|
-
def finished_request_message(reason = "Closed")
|
153
|
-
format(
|
154
|
-
'Finished "%s"%s for %s at %s (%s)',
|
155
|
-
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s, reason
|
156
|
-
)
|
157
|
-
end
|
158
|
-
|
159
|
-
def access_logs?
|
160
|
-
AnyCable.config.access_logs_disabled == false
|
161
|
-
end
|
162
|
-
|
163
|
-
def fetch_ltags
|
164
|
-
if instance_variable_defined?(:@logger)
|
165
|
-
logger.tags
|
166
|
-
else
|
167
|
-
ltags
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
def server
|
172
|
-
ActionCable.server
|
173
|
-
end
|
174
|
-
|
175
|
-
def verify_origin!
|
176
|
-
return true unless socket.env.key?("HTTP_ORIGIN")
|
177
|
-
|
178
|
-
return true if allow_request_origin?
|
179
|
-
|
180
|
-
reject_request(
|
181
|
-
ActionCable::INTERNAL[:disconnect_reasons]&.[](:invalid_request) || "invalid_request"
|
182
|
-
)
|
183
|
-
false
|
184
|
-
end
|
185
|
-
|
186
|
-
def reject_request(reason, reconnect = false)
|
187
|
-
logger.info finished_request_message("Rejected") if access_logs?
|
188
|
-
close(
|
189
|
-
reason: reason,
|
190
|
-
reconnect: reconnect
|
191
|
-
)
|
192
|
-
end
|
193
|
-
|
194
|
-
def build_rack_request
|
195
|
-
environment = Rails.application.env_config.merge(socket.env)
|
196
|
-
AnyCable::Rails::Rack.app.call(environment)
|
197
|
-
|
198
|
-
ActionDispatch::Request.new(environment)
|
199
|
-
end
|
200
|
-
|
201
|
-
def request_loaded?
|
202
|
-
instance_variable_defined?(:@request)
|
203
|
-
end
|
204
|
-
end
|
205
|
-
# rubocop:enable Metrics/ClassLength
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
# Support rescue_from
|
210
|
-
# https://github.com/rails/rails/commit/d2571e560c62116f60429c933d0c41a0e249b58b
|
211
|
-
if ActionCable::Connection::Base.respond_to?(:rescue_from)
|
212
|
-
ActionCable::Connection::Base.prepend(Module.new do
|
213
|
-
def handle_channel_command(*)
|
214
|
-
super
|
215
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
216
|
-
rescue_with_handler(e) || raise
|
217
|
-
false
|
218
|
-
end
|
219
|
-
end)
|
220
|
-
end
|
221
|
-
|
222
|
-
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 # rubocop:disable Lint/Void
|
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: {})
|
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
|