actioncable 5.0.1 → 6.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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']}"
|