anycable-rails-core 1.4.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 +7 -0
- data/CHANGELOG.md +203 -0
- data/MIT-LICENSE +20 -0
- data/README.md +106 -0
- data/lib/action_cable/subscription_adapter/any_cable.rb +40 -0
- data/lib/action_cable/subscription_adapter/anycable.rb +10 -0
- data/lib/anycable/rails/action_cable_ext/channel.rb +51 -0
- data/lib/anycable/rails/action_cable_ext/connection.rb +90 -0
- data/lib/anycable/rails/action_cable_ext/remote_connections.rb +13 -0
- data/lib/anycable/rails/channel_state.rb +108 -0
- data/lib/anycable/rails/compatibility/rubocop/config/default.yml +14 -0
- data/lib/anycable/rails/compatibility/rubocop/cops/anycable/instance_vars.rb +50 -0
- data/lib/anycable/rails/compatibility/rubocop/cops/anycable/periodical_timers.rb +29 -0
- data/lib/anycable/rails/compatibility/rubocop/cops/anycable/stream_from.rb +100 -0
- data/lib/anycable/rails/compatibility/rubocop.rb +27 -0
- data/lib/anycable/rails/compatibility.rb +63 -0
- data/lib/anycable/rails/config.rb +19 -0
- 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/executor.rb +31 -0
- data/lib/anycable/rails/middlewares/log_tagging.rb +21 -0
- data/lib/anycable/rails/rack.rb +56 -0
- data/lib/anycable/rails/railtie.rb +92 -0
- data/lib/anycable/rails/version.rb +7 -0
- data/lib/anycable/rails.rb +76 -0
- data/lib/anycable-rails.rb +3 -0
- data/lib/generators/anycable/download/USAGE +14 -0
- data/lib/generators/anycable/download/download_generator.rb +85 -0
- data/lib/generators/anycable/setup/USAGE +2 -0
- data/lib/generators/anycable/setup/setup_generator.rb +300 -0
- data/lib/generators/anycable/setup/templates/Procfile.dev.tt +6 -0
- data/lib/generators/anycable/setup/templates/config/anycable.yml.tt +48 -0
- data/lib/generators/anycable/setup/templates/config/cable.yml.tt +11 -0
- data/lib/generators/anycable/setup/templates/config/initializers/anycable.rb.tt +9 -0
- data/lib/generators/anycable/with_os_helpers.rb +55 -0
- metadata +128 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module AnyCable
|
8
|
+
# Checks for instance variable usage inside subscriptions.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# class MyChannel < ApplicationCable::Channel
|
13
|
+
# def subscribed
|
14
|
+
# @post = Post.find(params[:id])
|
15
|
+
# stream_from @post
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # good
|
20
|
+
# class MyChannel < ApplicationCable::Channel
|
21
|
+
# def subscribed
|
22
|
+
# post = Post.find(params[:id])
|
23
|
+
# stream_from post
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
class InstanceVars < RuboCop::Cop::Cop
|
28
|
+
MSG = "Channel instance variables are not supported in AnyCable. Use `state_attr_accessor` instead"
|
29
|
+
|
30
|
+
def on_class(node)
|
31
|
+
find_nested_ivars(node) do |nested_ivar|
|
32
|
+
add_offense(nested_ivar)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def find_nested_ivars(node, &block)
|
39
|
+
node.each_child_node do |child|
|
40
|
+
if child.ivasgn_type? || child.ivar_type?
|
41
|
+
yield(child)
|
42
|
+
elsif child.children.any?
|
43
|
+
find_nested_ivars(child, &block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module AnyCable
|
8
|
+
# Checks for periodical timers usage.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# class MyChannel < ApplicationCable::Channel
|
13
|
+
# periodically(:do_something, every: 2.seconds)
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
class PeriodicalTimers < RuboCop::Cop::Cop
|
17
|
+
MSG = "Periodical Timers are not supported in AnyCable"
|
18
|
+
|
19
|
+
def_node_matcher :calls_periodically?, <<-PATTERN
|
20
|
+
(send _ :periodically ...)
|
21
|
+
PATTERN
|
22
|
+
|
23
|
+
def on_send(node)
|
24
|
+
add_offense(node) if calls_periodically?(node)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Cop
|
7
|
+
module AnyCable
|
8
|
+
# Checks for #stream_from calls with custom callbacks or coders.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# class MyChannel < ApplicationCable::Channel
|
13
|
+
# def follow
|
14
|
+
# stream_for(room) {}
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# class MyChannel < ApplicationCable::Channel
|
19
|
+
# def follow
|
20
|
+
# stream_from("all", -> {})
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# class MyChannel < ApplicationCable::Channel
|
25
|
+
# def follow
|
26
|
+
# stream_from("all", coder: SomeCoder)
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# # good
|
31
|
+
# class MyChannel < ApplicationCable::Channel
|
32
|
+
# def follow
|
33
|
+
# stream_from "all"
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
class StreamFrom < RuboCop::Cop::Cop
|
38
|
+
def_node_matcher :stream_from_with_block?, <<-PATTERN
|
39
|
+
(block {(send _ :stream_from ...) (send _ :stream_for ...)} ...)
|
40
|
+
PATTERN
|
41
|
+
|
42
|
+
def_node_matcher :stream_from_with_callback?, <<-PATTERN
|
43
|
+
{(send _ :stream_from str_type? (block (send nil? :lambda) ...)) (send _ :stream_for ... (block (send nil? :lambda) ...))}
|
44
|
+
PATTERN
|
45
|
+
|
46
|
+
def_node_matcher :args_of_stream_from, <<-PATTERN
|
47
|
+
{(send _ :stream_from str_type? $...) (send _ :stream_for $...)}
|
48
|
+
PATTERN
|
49
|
+
|
50
|
+
def_node_matcher :coder_symbol?, "(pair (sym :coder) ...)"
|
51
|
+
|
52
|
+
def_node_matcher :active_support_json?, <<-PATTERN
|
53
|
+
(pair _ (const (const nil? :ActiveSupport) :JSON))
|
54
|
+
PATTERN
|
55
|
+
|
56
|
+
def on_block(node)
|
57
|
+
add_callback_offense(node) if stream_from_with_block?(node)
|
58
|
+
end
|
59
|
+
|
60
|
+
def on_send(node)
|
61
|
+
if stream_from_with_callback?(node)
|
62
|
+
add_callback_offense(node)
|
63
|
+
return
|
64
|
+
end
|
65
|
+
|
66
|
+
args = args_of_stream_from(node)
|
67
|
+
find_coders(args) { |coder| add_custom_coder_offense(coder) }
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def find_coders(args)
|
73
|
+
return if args.nil?
|
74
|
+
|
75
|
+
args.select(&:hash_type?).each do |arg|
|
76
|
+
arg.each_child_node do |pair|
|
77
|
+
yield(pair) if coder_symbol?(pair) && !active_support_json?(pair)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_callback_offense(node)
|
83
|
+
add_offense(
|
84
|
+
node,
|
85
|
+
location: :expression,
|
86
|
+
message: "Custom stream callbacks are not supported in AnyCable"
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_custom_coder_offense(node)
|
91
|
+
add_offense(
|
92
|
+
node,
|
93
|
+
location: :expression,
|
94
|
+
message: "Custom coders are not supported in AnyCable"
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rubocop"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
require_relative "rubocop/cops/anycable/stream_from"
|
7
|
+
require_relative "rubocop/cops/anycable/periodical_timers"
|
8
|
+
require_relative "rubocop/cops/anycable/instance_vars"
|
9
|
+
|
10
|
+
module RuboCop
|
11
|
+
module AnyCable # :nodoc:
|
12
|
+
CONFIG_DEFAULT = Pathname.new(__dir__).join("rubocop", "config", "default.yml").freeze
|
13
|
+
|
14
|
+
# Merge anycable config into default configuration
|
15
|
+
# See https://github.com/backus/rubocop-rspec/blob/master/lib/rubocop/rspec/inject.rb
|
16
|
+
def self.inject!
|
17
|
+
path = CONFIG_DEFAULT.to_s
|
18
|
+
puts "configuration from #{path}" if ConfigLoader.debug?
|
19
|
+
hash = ConfigLoader.send(:load_yaml_configuration, path)
|
20
|
+
config = Config.new(hash, path)
|
21
|
+
config = ConfigLoader.merge_with_default(config, path)
|
22
|
+
ConfigLoader.instance_variable_set(:@default_configuration, config)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
RuboCop::AnyCable.inject!
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AnyCable
|
4
|
+
class CompatibilityError < StandardError; end
|
5
|
+
|
6
|
+
module Compatibility # :nodoc:
|
7
|
+
IGNORE_INSTANCE_VARS = %i[
|
8
|
+
@active_periodic_timers
|
9
|
+
@_streams
|
10
|
+
@parameter_filter
|
11
|
+
]
|
12
|
+
|
13
|
+
ActionCable::Channel::Base.prepend(Module.new do
|
14
|
+
def stream_from(broadcasting, callback = nil, coder: nil)
|
15
|
+
if coder.present? && coder != ActiveSupport::JSON
|
16
|
+
raise AnyCable::CompatibilityError, "Custom coders are not supported by AnyCable"
|
17
|
+
end
|
18
|
+
|
19
|
+
if callback.present? || block_given?
|
20
|
+
raise AnyCable::CompatibilityError,
|
21
|
+
"Custom stream callbacks are not supported by AnyCable"
|
22
|
+
end
|
23
|
+
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
# Do not prepend `subscribe_to_channel` 'cause we make it no-op
|
28
|
+
# when AnyCable is running (see anycable/rails/actioncable/channel.rb)
|
29
|
+
%w[run_callbacks perform_action].each do |mid|
|
30
|
+
module_eval <<~CODE, __FILE__, __LINE__ + 1
|
31
|
+
def #{mid}(*)
|
32
|
+
__anycable_check_ivars__ { super }
|
33
|
+
end
|
34
|
+
CODE
|
35
|
+
end
|
36
|
+
|
37
|
+
def __anycable_check_ivars__
|
38
|
+
was_ivars = instance_variables
|
39
|
+
res = yield
|
40
|
+
diff = instance_variables - was_ivars - IGNORE_INSTANCE_VARS
|
41
|
+
|
42
|
+
if self.class.respond_to?(:channel_state_attributes)
|
43
|
+
diff.delete(:@__istate__)
|
44
|
+
diff.delete_if { |ivar| self.class.channel_state_attributes.include?(:"#{ivar.to_s.sub(/^@/, "")}") }
|
45
|
+
end
|
46
|
+
|
47
|
+
unless diff.empty?
|
48
|
+
raise AnyCable::CompatibilityError,
|
49
|
+
"Channel instance variables are not supported by AnyCable, " \
|
50
|
+
"but were set: #{diff.join(", ")}"
|
51
|
+
end
|
52
|
+
|
53
|
+
res
|
54
|
+
end
|
55
|
+
end)
|
56
|
+
|
57
|
+
ActionCable::Channel::Base.singleton_class.prepend(Module.new do
|
58
|
+
def periodically(*)
|
59
|
+
raise AnyCable::CompatibilityError, "Periodical timers are not supported by AnyCable"
|
60
|
+
end
|
61
|
+
end)
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "anycable/config"
|
4
|
+
# Make sure Rails extensions for Anyway Config are loaded
|
5
|
+
# See https://github.com/anycable/anycable-rails/issues/63
|
6
|
+
require "anyway/rails"
|
7
|
+
|
8
|
+
# Extend AnyCable configuration with:
|
9
|
+
# - `access_logs_disabled` (defaults to true) — whether to print Started/Finished logs
|
10
|
+
# - `persistent_session_enabled` (defaults to false) — whether to store session changes in the connection state
|
11
|
+
# - `embedded` (defaults to false) — whether to run RPC server inside a Rails server process
|
12
|
+
# - `http_rpc_mount_path` (default to nil) — path to mount HTTP RPC server
|
13
|
+
AnyCable::Config.attr_config(
|
14
|
+
access_logs_disabled: true,
|
15
|
+
persistent_session_enabled: false,
|
16
|
+
embedded: false,
|
17
|
+
http_rpc_mount_path: nil
|
18
|
+
)
|
19
|
+
AnyCable::Config.ignore_options :access_logs_disabled, :persistent_session_enabled
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_cable"
|
4
|
+
|
5
|
+
module AnyCable
|
6
|
+
module Rails
|
7
|
+
# Enhance Action Cable connection
|
8
|
+
using(Module.new do
|
9
|
+
refine ActionCable::Connection::Base do
|
10
|
+
attr_writer :env, :websocket, :logger, :coder,
|
11
|
+
:subscriptions, :serialized_ids, :cached_ids, :server,
|
12
|
+
:anycable_socket
|
13
|
+
|
14
|
+
# Using public :send_welcome_message causes stack level too deep 🤷🏻♂️
|
15
|
+
def send_welcome_message
|
16
|
+
transmit({
|
17
|
+
type: ActionCable::INTERNAL[:message_types][:welcome],
|
18
|
+
sid: env["anycable.sid"]
|
19
|
+
}.compact)
|
20
|
+
end
|
21
|
+
|
22
|
+
def public_request
|
23
|
+
request
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
refine ActionCable::Channel::Base do
|
28
|
+
def rejected?
|
29
|
+
subscription_rejected?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
refine ActionCable::Connection::Subscriptions do
|
34
|
+
# Find or add a subscription to the list
|
35
|
+
def fetch(identifier)
|
36
|
+
add("identifier" => identifier) unless subscriptions[identifier]
|
37
|
+
|
38
|
+
unless subscriptions[identifier]
|
39
|
+
raise "Channel not found: #{ActiveSupport::JSON.decode(identifier).fetch("channel")}"
|
40
|
+
end
|
41
|
+
|
42
|
+
subscriptions[identifier]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end)
|
46
|
+
|
47
|
+
class Connection
|
48
|
+
# We store logger tags in the connection state to be able
|
49
|
+
# to re-use them in the subsequent calls
|
50
|
+
LOG_TAGS_IDENTIFIER = "__ltags__"
|
51
|
+
|
52
|
+
delegate :identifiers_json, to: :conn
|
53
|
+
|
54
|
+
attr_reader :socket, :logger
|
55
|
+
|
56
|
+
def initialize(connection_class, socket, identifiers: nil, subscriptions: nil)
|
57
|
+
@socket = socket
|
58
|
+
|
59
|
+
logger_tags = fetch_logger_tags_from_state
|
60
|
+
@logger = ActionCable::Connection::TaggedLoggerProxy.new(AnyCable.logger, tags: logger_tags)
|
61
|
+
|
62
|
+
# Instead of calling #initialize,
|
63
|
+
# we allocate an instance and setup all the required components manually
|
64
|
+
@conn = connection_class.allocate
|
65
|
+
# Required to access config (for access origin checks)
|
66
|
+
conn.server = ActionCable.server
|
67
|
+
conn.logger = logger
|
68
|
+
conn.anycable_socket = conn.websocket = socket
|
69
|
+
conn.env = socket.env
|
70
|
+
conn.coder = ActiveSupport::JSON
|
71
|
+
conn.subscriptions = ActionCable::Connection::Subscriptions.new(conn)
|
72
|
+
conn.serialized_ids = {}
|
73
|
+
conn.serialized_ids = ActiveSupport::JSON.decode(identifiers) if identifiers
|
74
|
+
conn.cached_ids = {}
|
75
|
+
conn.anycable_request_builder = self
|
76
|
+
|
77
|
+
return unless subscriptions
|
78
|
+
|
79
|
+
# Pre-initialize channels (for disconnect)
|
80
|
+
subscriptions.each do |id|
|
81
|
+
channel = conn.subscriptions.fetch(id)
|
82
|
+
next unless socket.istate[id]
|
83
|
+
|
84
|
+
channel.__istate__ = ActiveSupport::JSON.decode(socket.istate[id])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def handle_open
|
89
|
+
logger.info started_request_message if access_logs?
|
90
|
+
|
91
|
+
verify_origin! || return
|
92
|
+
|
93
|
+
conn.connect if conn.respond_to?(:connect)
|
94
|
+
|
95
|
+
socket.cstate.write(LOG_TAGS_IDENTIFIER, logger.tags.to_json) unless logger.tags.empty?
|
96
|
+
|
97
|
+
conn.send_welcome_message
|
98
|
+
rescue ::ActionCable::Connection::Authorization::UnauthorizedError
|
99
|
+
reject_request(
|
100
|
+
ActionCable::INTERNAL[:disconnect_reasons]&.[](:unauthorized) || "unauthorized"
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
def handle_close
|
105
|
+
logger.info finished_request_message if access_logs?
|
106
|
+
|
107
|
+
conn.subscriptions.unsubscribe_from_all
|
108
|
+
conn.disconnect if conn.respond_to?(:disconnect)
|
109
|
+
true
|
110
|
+
end
|
111
|
+
|
112
|
+
def handle_channel_command(identifier, command, data)
|
113
|
+
conn.run_callbacks :command do
|
114
|
+
# We cannot use subscriptions#execute_command here,
|
115
|
+
# since we MUST return true of false, depending on the status
|
116
|
+
# of execution
|
117
|
+
channel = conn.subscriptions.fetch(identifier)
|
118
|
+
case command
|
119
|
+
when "subscribe"
|
120
|
+
channel.handle_subscribe
|
121
|
+
!channel.rejected?
|
122
|
+
when "unsubscribe"
|
123
|
+
conn.subscriptions.remove_subscription(channel)
|
124
|
+
true
|
125
|
+
when "message"
|
126
|
+
channel.perform_action ActiveSupport::JSON.decode(data)
|
127
|
+
true
|
128
|
+
else
|
129
|
+
false
|
130
|
+
end
|
131
|
+
end
|
132
|
+
# Support rescue_from
|
133
|
+
# https://github.com/rails/rails/commit/d2571e560c62116f60429c933d0c41a0e249b58b
|
134
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
135
|
+
rescue_with_handler(e) || raise
|
136
|
+
false
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_rack_request(env)
|
140
|
+
environment = ::Rails.application.env_config.merge(env) if defined?(::Rails.application) && ::Rails.application
|
141
|
+
AnyCable::Rails::Rack.app.call(environment) if environment
|
142
|
+
|
143
|
+
ActionDispatch::Request.new(environment || env)
|
144
|
+
end
|
145
|
+
|
146
|
+
def action_cable_connection
|
147
|
+
conn
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
attr_reader :conn
|
153
|
+
|
154
|
+
def reject_request(reason, reconnect = false)
|
155
|
+
logger.info finished_request_message("Rejected") if access_logs?
|
156
|
+
conn.close(
|
157
|
+
reason: reason,
|
158
|
+
reconnect: reconnect
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
def fetch_logger_tags_from_state
|
163
|
+
socket.cstate.read(LOG_TAGS_IDENTIFIER).yield_self do |raw_tags|
|
164
|
+
next [] unless raw_tags
|
165
|
+
ActiveSupport::JSON.decode(raw_tags)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def started_request_message
|
170
|
+
format(
|
171
|
+
'Started "%s"%s for %s at %s',
|
172
|
+
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s
|
173
|
+
)
|
174
|
+
end
|
175
|
+
|
176
|
+
def finished_request_message(reason = "Closed")
|
177
|
+
format(
|
178
|
+
'Finished "%s"%s for %s at %s (%s)',
|
179
|
+
request.filtered_path, " [AnyCable]", request.ip, Time.now.to_s, reason
|
180
|
+
)
|
181
|
+
end
|
182
|
+
|
183
|
+
def verify_origin!
|
184
|
+
return true unless socket.env.key?("HTTP_ORIGIN")
|
185
|
+
|
186
|
+
return true if conn.send(:allow_request_origin?)
|
187
|
+
|
188
|
+
reject_request(
|
189
|
+
ActionCable::INTERNAL[:disconnect_reasons]&.[](:invalid_request) || "invalid_request"
|
190
|
+
)
|
191
|
+
false
|
192
|
+
end
|
193
|
+
|
194
|
+
def access_logs?
|
195
|
+
AnyCable.config.access_logs_disabled == false
|
196
|
+
end
|
197
|
+
|
198
|
+
def request
|
199
|
+
conn.public_request
|
200
|
+
end
|
201
|
+
|
202
|
+
def request_loaded?
|
203
|
+
conn.instance_variable_defined?(:@request)
|
204
|
+
end
|
205
|
+
|
206
|
+
def rescue_with_handler(e)
|
207
|
+
conn.rescue_with_handler(e) if conn.respond_to?(:rescue_with_handler)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
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
|