actioncable 5.0.1 → 6.1.3
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 +5 -5
- data/CHANGELOG.md +31 -117
- data/MIT-LICENSE +1 -1
- data/README.md +4 -535
- data/app/assets/javascripts/action_cable.js +517 -0
- data/lib/action_cable.rb +20 -10
- data/lib/action_cable/channel.rb +3 -0
- data/lib/action_cable/channel/base.rb +31 -23
- data/lib/action_cable/channel/broadcasting.rb +22 -10
- data/lib/action_cable/channel/callbacks.rb +4 -2
- data/lib/action_cable/channel/naming.rb +5 -2
- data/lib/action_cable/channel/periodic_timers.rb +4 -3
- data/lib/action_cable/channel/streams.rb +39 -11
- data/lib/action_cable/channel/test_case.rb +310 -0
- data/lib/action_cable/connection.rb +3 -2
- data/lib/action_cable/connection/authorization.rb +8 -6
- data/lib/action_cable/connection/base.rb +34 -26
- data/lib/action_cable/connection/client_socket.rb +20 -18
- data/lib/action_cable/connection/identification.rb +5 -4
- data/lib/action_cable/connection/internal_channel.rb +4 -2
- data/lib/action_cable/connection/message_buffer.rb +3 -2
- data/lib/action_cable/connection/stream.rb +9 -5
- data/lib/action_cable/connection/stream_event_loop.rb +4 -2
- data/lib/action_cable/connection/subscriptions.rb +14 -13
- data/lib/action_cable/connection/tagged_logger_proxy.rb +4 -2
- data/lib/action_cable/connection/test_case.rb +234 -0
- data/lib/action_cable/connection/web_socket.rb +7 -5
- data/lib/action_cable/engine.rb +7 -5
- data/lib/action_cable/gem_version.rb +5 -3
- data/lib/action_cable/helpers/action_cable_helper.rb +6 -4
- data/lib/action_cable/remote_connections.rb +9 -4
- data/lib/action_cable/server.rb +2 -1
- data/lib/action_cable/server/base.rb +17 -10
- data/lib/action_cable/server/broadcasting.rb +9 -3
- data/lib/action_cable/server/configuration.rb +21 -22
- data/lib/action_cable/server/connections.rb +2 -0
- data/lib/action_cable/server/worker.rb +11 -11
- data/lib/action_cable/server/worker/active_record_connection_management.rb +2 -0
- data/lib/action_cable/subscription_adapter.rb +4 -0
- data/lib/action_cable/subscription_adapter/async.rb +3 -1
- data/lib/action_cable/subscription_adapter/base.rb +6 -0
- data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
- data/lib/action_cable/subscription_adapter/inline.rb +2 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +40 -14
- data/lib/action_cable/subscription_adapter/redis.rb +19 -11
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +3 -1
- data/lib/action_cable/subscription_adapter/test.rb +40 -0
- data/lib/action_cable/test_case.rb +11 -0
- data/lib/action_cable/test_helper.rb +133 -0
- data/lib/action_cable/version.rb +3 -1
- data/lib/rails/generators/channel/USAGE +5 -6
- data/lib/rails/generators/channel/channel_generator.rb +16 -11
- data/lib/rails/generators/channel/templates/application_cable/{channel.rb → channel.rb.tt} +0 -0
- data/lib/rails/generators/channel/templates/application_cable/{connection.rb → connection.rb.tt} +0 -0
- data/lib/rails/generators/channel/templates/{channel.rb → channel.rb.tt} +0 -0
- data/lib/rails/generators/channel/templates/{assets/channel.js → javascript/channel.js.tt} +6 -4
- data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
- data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
- data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
- data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
- metadata +46 -38
- data/lib/action_cable/connection/faye_client_socket.rb +0 -48
- data/lib/action_cable/connection/faye_event_loop.rb +0 -44
- data/lib/action_cable/subscription_adapter/evented_redis.rb +0 -79
- data/lib/assets/compiled/action_cable.js +0 -597
- data/lib/rails/generators/channel/templates/assets/cable.js +0 -13
- data/lib/rails/generators/channel/templates/assets/channel.coffee +0 -14
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionCable
|
2
4
|
module Connection
|
3
5
|
extend ActiveSupport::Autoload
|
@@ -8,13 +10,12 @@ module ActionCable
|
|
8
10
|
autoload :ClientSocket
|
9
11
|
autoload :Identification
|
10
12
|
autoload :InternalChannel
|
11
|
-
autoload :FayeClientSocket
|
12
|
-
autoload :FayeEventLoop
|
13
13
|
autoload :MessageBuffer
|
14
14
|
autoload :Stream
|
15
15
|
autoload :StreamEventLoop
|
16
16
|
autoload :Subscriptions
|
17
17
|
autoload :TaggedLoggerProxy
|
18
|
+
autoload :TestCase
|
18
19
|
autoload :WebSocket
|
19
20
|
end
|
20
21
|
end
|
@@ -1,13 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionCable
|
2
4
|
module Connection
|
3
5
|
module Authorization
|
4
6
|
class UnauthorizedError < StandardError; end
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
# Closes the WebSocket connection if it is open and returns a 404 "File not Found" response.
|
9
|
+
def reject_unauthorized_connection
|
10
|
+
logger.error "An unauthorized connection attempt was rejected"
|
11
|
+
raise UnauthorizedError
|
12
|
+
end
|
11
13
|
end
|
12
14
|
end
|
13
|
-
end
|
15
|
+
end
|
@@ -1,8 +1,11 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_dispatch"
|
4
|
+
require "active_support/rescuable"
|
2
5
|
|
3
6
|
module ActionCable
|
4
7
|
module Connection
|
5
|
-
# For every WebSocket the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
|
8
|
+
# For every WebSocket connection the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
|
6
9
|
# of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
|
7
10
|
# based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
|
8
11
|
# authentication and authorization.
|
@@ -22,13 +25,10 @@ module ActionCable
|
|
22
25
|
# # Any cleanup work needed when the cable connection is cut.
|
23
26
|
# end
|
24
27
|
#
|
25
|
-
#
|
28
|
+
# private
|
26
29
|
# def find_verified_user
|
27
|
-
#
|
28
|
-
# current_user
|
29
|
-
# else
|
30
|
+
# User.find_by_identity(cookies.encrypted[:identity_id]) ||
|
30
31
|
# reject_unauthorized_connection
|
31
|
-
# end
|
32
32
|
# end
|
33
33
|
# end
|
34
34
|
# end
|
@@ -47,6 +47,7 @@ module ActionCable
|
|
47
47
|
include Identification
|
48
48
|
include InternalChannel
|
49
49
|
include Authorization
|
50
|
+
include ActiveSupport::Rescuable
|
50
51
|
|
51
52
|
attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
|
52
53
|
delegate :event_loop, :pubsub, to: :server
|
@@ -57,7 +58,7 @@ module ActionCable
|
|
57
58
|
@worker_pool = server.worker_pool
|
58
59
|
@logger = new_tagged_logger
|
59
60
|
|
60
|
-
@websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop
|
61
|
+
@websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop)
|
61
62
|
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
|
62
63
|
@message_buffer = ActionCable::Connection::MessageBuffer.new(self)
|
63
64
|
|
@@ -96,7 +97,12 @@ module ActionCable
|
|
96
97
|
end
|
97
98
|
|
98
99
|
# Close the WebSocket connection.
|
99
|
-
def close
|
100
|
+
def close(reason: nil, reconnect: true)
|
101
|
+
transmit(
|
102
|
+
type: ActionCable::INTERNAL[:message_types][:disconnect],
|
103
|
+
reason: reason,
|
104
|
+
reconnect: reconnect
|
105
|
+
)
|
100
106
|
websocket.close
|
101
107
|
end
|
102
108
|
|
@@ -105,14 +111,14 @@ module ActionCable
|
|
105
111
|
worker_pool.async_invoke(self, method, *arguments)
|
106
112
|
end
|
107
113
|
|
108
|
-
# Return a basic hash of statistics for the connection keyed with
|
114
|
+
# Return a basic hash of statistics for the connection keyed with <tt>identifier</tt>, <tt>started_at</tt>, <tt>subscriptions</tt>, and <tt>request_id</tt>.
|
109
115
|
# This can be returned by a health check against the connection.
|
110
116
|
def statistics
|
111
117
|
{
|
112
118
|
identifier: connection_identifier,
|
113
119
|
started_at: @started_at,
|
114
120
|
subscriptions: subscriptions.identifiers,
|
115
|
-
request_id: @env[
|
121
|
+
request_id: @env["action_dispatch.request_id"]
|
116
122
|
}
|
117
123
|
end
|
118
124
|
|
@@ -129,16 +135,20 @@ module ActionCable
|
|
129
135
|
end
|
130
136
|
|
131
137
|
def on_error(message) # :nodoc:
|
132
|
-
#
|
138
|
+
# log errors to make diagnosing socket errors easier
|
139
|
+
logger.error "WebSocket error occurred: #{message}"
|
133
140
|
end
|
134
141
|
|
135
142
|
def on_close(reason, code) # :nodoc:
|
136
143
|
send_async :handle_close
|
137
144
|
end
|
138
145
|
|
139
|
-
|
146
|
+
private
|
147
|
+
attr_reader :websocket
|
148
|
+
attr_reader :message_buffer
|
149
|
+
|
140
150
|
# The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc.
|
141
|
-
def request
|
151
|
+
def request # :doc:
|
142
152
|
@request ||= begin
|
143
153
|
environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
|
144
154
|
ActionDispatch::Request.new(environment || env)
|
@@ -146,14 +156,10 @@ module ActionCable
|
|
146
156
|
end
|
147
157
|
|
148
158
|
# The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks.
|
149
|
-
def cookies
|
159
|
+
def cookies # :doc:
|
150
160
|
request.cookie_jar
|
151
161
|
end
|
152
162
|
|
153
|
-
attr_reader :websocket
|
154
|
-
attr_reader :message_buffer
|
155
|
-
|
156
|
-
private
|
157
163
|
def encode(cable_message)
|
158
164
|
@coder.encode cable_message
|
159
165
|
end
|
@@ -171,7 +177,7 @@ module ActionCable
|
|
171
177
|
message_buffer.process!
|
172
178
|
server.add_connection(self)
|
173
179
|
rescue ActionCable::Connection::Authorization::UnauthorizedError
|
174
|
-
|
180
|
+
close(reason: ActionCable::INTERNAL[:disconnect_reasons][:unauthorized], reconnect: false) if websocket.alive?
|
175
181
|
end
|
176
182
|
|
177
183
|
def handle_close
|
@@ -212,11 +218,11 @@ module ActionCable
|
|
212
218
|
end
|
213
219
|
|
214
220
|
def respond_to_invalid_request
|
215
|
-
close if websocket.alive?
|
221
|
+
close(reason: ActionCable::INTERNAL[:disconnect_reasons][:invalid_request]) if websocket.alive?
|
216
222
|
|
217
223
|
logger.error invalid_request_message
|
218
224
|
logger.info finished_request_message
|
219
|
-
[ 404, {
|
225
|
+
[ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ]
|
220
226
|
end
|
221
227
|
|
222
228
|
# Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags.
|
@@ -229,7 +235,7 @@ module ActionCable
|
|
229
235
|
'Started %s "%s"%s for %s at %s' % [
|
230
236
|
request.request_method,
|
231
237
|
request.filtered_path,
|
232
|
-
websocket.possible? ?
|
238
|
+
websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
|
233
239
|
request.ip,
|
234
240
|
Time.now.to_s ]
|
235
241
|
end
|
@@ -237,22 +243,24 @@ module ActionCable
|
|
237
243
|
def finished_request_message
|
238
244
|
'Finished "%s"%s for %s at %s' % [
|
239
245
|
request.filtered_path,
|
240
|
-
websocket.possible? ?
|
246
|
+
websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
|
241
247
|
request.ip,
|
242
248
|
Time.now.to_s ]
|
243
249
|
end
|
244
250
|
|
245
251
|
def invalid_request_message
|
246
|
-
|
252
|
+
"Failed to upgrade to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
|
247
253
|
env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
|
248
254
|
]
|
249
255
|
end
|
250
256
|
|
251
257
|
def successful_request_message
|
252
|
-
|
258
|
+
"Successfully upgraded to WebSocket (REQUEST_METHOD: %s, HTTP_CONNECTION: %s, HTTP_UPGRADE: %s)" % [
|
253
259
|
env["REQUEST_METHOD"], env["HTTP_CONNECTION"], env["HTTP_UPGRADE"]
|
254
260
|
]
|
255
261
|
end
|
256
262
|
end
|
257
263
|
end
|
258
264
|
end
|
265
|
+
|
266
|
+
ActiveSupport.run_load_hooks(:action_cable_connection, ActionCable::Connection::Base)
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "websocket/driver"
|
2
4
|
|
3
5
|
module ActionCable
|
4
6
|
module Connection
|
@@ -8,18 +10,18 @@ module ActionCable
|
|
8
10
|
# Copyright (c) 2010-2015 James Coglan
|
9
11
|
class ClientSocket # :nodoc:
|
10
12
|
def self.determine_url(env)
|
11
|
-
scheme = secure_request?(env) ?
|
13
|
+
scheme = secure_request?(env) ? "wss:" : "ws:"
|
12
14
|
"#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
|
13
15
|
end
|
14
16
|
|
15
17
|
def self.secure_request?(env)
|
16
|
-
return true if env[
|
17
|
-
return true if env[
|
18
|
-
return true if env[
|
19
|
-
return true if env[
|
20
|
-
return true if env[
|
18
|
+
return true if env["HTTPS"] == "on"
|
19
|
+
return true if env["HTTP_X_FORWARDED_SSL"] == "on"
|
20
|
+
return true if env["HTTP_X_FORWARDED_SCHEME"] == "https"
|
21
|
+
return true if env["HTTP_X_FORWARDED_PROTO"] == "https"
|
22
|
+
return true if env["rack.url_scheme"] == "https"
|
21
23
|
|
22
|
-
|
24
|
+
false
|
23
25
|
end
|
24
26
|
|
25
27
|
CONNECTING = 0
|
@@ -37,7 +39,7 @@ module ActionCable
|
|
37
39
|
@url = ClientSocket.determine_url(@env)
|
38
40
|
|
39
41
|
@driver = @driver_started = nil
|
40
|
-
@close_params = [
|
42
|
+
@close_params = ["", 1006]
|
41
43
|
|
42
44
|
@ready_state = CONNECTING
|
43
45
|
|
@@ -56,7 +58,7 @@ module ActionCable
|
|
56
58
|
return if @driver.nil? || @driver_started
|
57
59
|
@stream.hijack_rack_socket
|
58
60
|
|
59
|
-
if callback = @env[
|
61
|
+
if callback = @env["async.callback"]
|
60
62
|
callback.call([101, {}, @stream])
|
61
63
|
end
|
62
64
|
|
@@ -78,20 +80,20 @@ module ActionCable
|
|
78
80
|
def transmit(message)
|
79
81
|
return false if @ready_state > OPEN
|
80
82
|
case message
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
83
|
+
when Numeric then @driver.text(message.to_s)
|
84
|
+
when String then @driver.text(message)
|
85
|
+
when Array then @driver.binary(message)
|
86
|
+
else false
|
85
87
|
end
|
86
88
|
end
|
87
89
|
|
88
90
|
def close(code = nil, reason = nil)
|
89
91
|
code ||= 1000
|
90
|
-
reason ||=
|
92
|
+
reason ||= ""
|
91
93
|
|
92
|
-
unless code == 1000
|
93
|
-
raise ArgumentError, "Failed to execute 'close' on WebSocket: "
|
94
|
-
"The code must be either 1000, or between 3000 and 4999. "
|
94
|
+
unless code == 1000 || (code >= 3000 && code <= 4999)
|
95
|
+
raise ArgumentError, "Failed to execute 'close' on WebSocket: " \
|
96
|
+
"The code must be either 1000, or between 3000 and 4999. " \
|
95
97
|
"#{code} is neither."
|
96
98
|
end
|
97
99
|
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
2
4
|
|
3
5
|
module ActionCable
|
4
6
|
module Connection
|
@@ -6,11 +8,10 @@ module ActionCable
|
|
6
8
|
extend ActiveSupport::Concern
|
7
9
|
|
8
10
|
included do
|
9
|
-
class_attribute :identifiers
|
10
|
-
self.identifiers = Set.new
|
11
|
+
class_attribute :identifiers, default: Set.new
|
11
12
|
end
|
12
13
|
|
13
|
-
|
14
|
+
module ClassMethods
|
14
15
|
# Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
|
15
16
|
# Common identifiers are current_user and current_account, but could be anything, really.
|
16
17
|
#
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionCable
|
2
4
|
module Connection
|
3
5
|
# Makes it possible for the RemoteConnection to disconnect a specific connection.
|
@@ -27,8 +29,8 @@ module ActionCable
|
|
27
29
|
end
|
28
30
|
|
29
31
|
def process_internal_message(message)
|
30
|
-
case message[
|
31
|
-
when
|
32
|
+
case message["type"]
|
33
|
+
when "disconnect"
|
32
34
|
logger.info "Removing connection (#{connection_identifier})"
|
33
35
|
websocket.close
|
34
36
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionCable
|
2
4
|
module Connection
|
3
5
|
# Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized, and is ready to receive them.
|
@@ -28,11 +30,10 @@ module ActionCable
|
|
28
30
|
receive_buffered_messages
|
29
31
|
end
|
30
32
|
|
31
|
-
|
33
|
+
private
|
32
34
|
attr_reader :connection
|
33
35
|
attr_reader :buffered_messages
|
34
36
|
|
35
|
-
private
|
36
37
|
def valid?(message)
|
37
38
|
message.is_a?(String)
|
38
39
|
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thread"
|
2
4
|
|
3
5
|
module ActionCable
|
4
6
|
module Connection
|
@@ -10,7 +12,7 @@ module ActionCable
|
|
10
12
|
def initialize(event_loop, socket)
|
11
13
|
@event_loop = event_loop
|
12
14
|
@socket_object = socket
|
13
|
-
@stream_send = socket.env[
|
15
|
+
@stream_send = socket.env["stream.send"]
|
14
16
|
|
15
17
|
@rack_hijack_io = nil
|
16
18
|
@write_lock = Mutex.new
|
@@ -94,10 +96,12 @@ module ActionCable
|
|
94
96
|
end
|
95
97
|
|
96
98
|
def hijack_rack_socket
|
97
|
-
return unless @socket_object.env[
|
99
|
+
return unless @socket_object.env["rack.hijack"]
|
98
100
|
|
99
|
-
|
100
|
-
@rack_hijack_io = @socket_object.env[
|
101
|
+
# This should return the underlying io according to the SPEC:
|
102
|
+
@rack_hijack_io = @socket_object.env["rack.hijack"].call
|
103
|
+
# Retain existing behaviour if required:
|
104
|
+
@rack_hijack_io ||= @socket_object.env["rack.hijack_io"]
|
101
105
|
|
102
106
|
@event_loop.attach(@rack_hijack_io, self)
|
103
107
|
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/hash/indifferent_access"
|
2
4
|
|
3
5
|
module ActionCable
|
4
6
|
module Connection
|
@@ -11,19 +13,20 @@ module ActionCable
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def execute_command(data)
|
14
|
-
case data[
|
15
|
-
when
|
16
|
-
when
|
17
|
-
when
|
16
|
+
case data["command"]
|
17
|
+
when "subscribe" then add data
|
18
|
+
when "unsubscribe" then remove data
|
19
|
+
when "message" then perform_action data
|
18
20
|
else
|
19
21
|
logger.error "Received unrecognized command in #{data.inspect}"
|
20
22
|
end
|
21
23
|
rescue Exception => e
|
22
|
-
|
24
|
+
@connection.rescue_with_handler(e)
|
25
|
+
logger.error "Could not execute command from (#{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
|
23
26
|
end
|
24
27
|
|
25
28
|
def add(data)
|
26
|
-
id_key = data[
|
29
|
+
id_key = data["identifier"]
|
27
30
|
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
|
28
31
|
|
29
32
|
return if subscriptions.key?(id_key)
|
@@ -41,7 +44,7 @@ module ActionCable
|
|
41
44
|
|
42
45
|
def remove(data)
|
43
46
|
logger.info "Unsubscribing from channel: #{data['identifier']}"
|
44
|
-
remove_subscription
|
47
|
+
remove_subscription find(data)
|
45
48
|
end
|
46
49
|
|
47
50
|
def remove_subscription(subscription)
|
@@ -50,7 +53,7 @@ module ActionCable
|
|
50
53
|
end
|
51
54
|
|
52
55
|
def perform_action(data)
|
53
|
-
find(data).perform_action ActiveSupport::JSON.decode(data[
|
56
|
+
find(data).perform_action ActiveSupport::JSON.decode(data["data"])
|
54
57
|
end
|
55
58
|
|
56
59
|
def identifiers
|
@@ -61,14 +64,12 @@ module ActionCable
|
|
61
64
|
subscriptions.each { |id, channel| remove_subscription(channel) }
|
62
65
|
end
|
63
66
|
|
64
|
-
protected
|
65
|
-
attr_reader :connection, :subscriptions
|
66
|
-
|
67
67
|
private
|
68
|
+
attr_reader :connection, :subscriptions
|
68
69
|
delegate :logger, to: :connection
|
69
70
|
|
70
71
|
def find(data)
|
71
|
-
if subscription = subscriptions[data[
|
72
|
+
if subscription = subscriptions[data["identifier"]]
|
72
73
|
subscription
|
73
74
|
else
|
74
75
|
raise "Unable to find subscription with identifier: #{data['identifier']}"
|