ably 0.8.7 → 0.8.8
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 +61 -3
- data/ably.gemspec +1 -1
- data/lib/ably/auth.rb +12 -3
- data/lib/ably/logger.rb +3 -1
- data/lib/ably/models/connection_details.rb +2 -1
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +1 -1
- data/lib/ably/models/message.rb +4 -2
- data/lib/ably/models/message_encoders/cipher.rb +2 -2
- data/lib/ably/models/paginated_result.rb +27 -1
- data/lib/ably/models/presence_message.rb +3 -1
- data/lib/ably/models/{stat.rb → stats.rb} +5 -3
- data/lib/ably/modules/async_wrapper.rb +1 -1
- data/lib/ably/modules/channels_collection.rb +11 -1
- data/lib/ably/modules/enum.rb +18 -2
- data/lib/ably/modules/event_emitter.rb +3 -3
- data/lib/ably/modules/safe_deferrable.rb +1 -1
- data/lib/ably/modules/safe_yield.rb +2 -2
- data/lib/ably/modules/state_emitter.rb +8 -8
- data/lib/ably/modules/statesman_monkey_patch.rb +2 -2
- data/lib/ably/modules/uses_state_machine.rb +4 -2
- data/lib/ably/realtime.rb +1 -0
- data/lib/ably/realtime/auth.rb +6 -2
- data/lib/ably/realtime/channel.rb +6 -4
- data/lib/ably/realtime/channel/channel_manager.rb +7 -1
- data/lib/ably/realtime/client.rb +7 -12
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +9 -2
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +7 -1
- data/lib/ably/realtime/connection.rb +19 -8
- data/lib/ably/realtime/connection/connection_manager.rb +16 -9
- data/lib/ably/realtime/connection/websocket_transport.rb +12 -3
- data/lib/ably/realtime/presence.rb +6 -6
- data/lib/ably/realtime/presence/members_map.rb +21 -7
- data/lib/ably/rest/channel.rb +8 -8
- data/lib/ably/rest/client.rb +1 -1
- data/lib/ably/rest/middleware/exceptions.rb +3 -1
- data/lib/ably/rest/presence.rb +4 -4
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_history_spec.rb +4 -4
- data/spec/acceptance/realtime/connection_failures_spec.rb +2 -4
- data/spec/acceptance/realtime/connection_spec.rb +46 -8
- data/spec/acceptance/realtime/presence_spec.rb +49 -34
- data/spec/acceptance/rest/auth_spec.rb +1 -1
- data/spec/acceptance/rest/base_spec.rb +1 -1
- data/spec/shared/safe_deferrable_behaviour.rb +4 -4
- data/spec/unit/models/message_encoders/cipher_spec.rb +1 -1
- data/spec/unit/models/token_details_spec.rb +20 -18
- data/spec/unit/modules/event_emitter_spec.rb +2 -2
- data/spec/unit/modules/state_emitter_spec.rb +6 -6
- data/spec/unit/realtime/channel_spec.rb +4 -4
- data/spec/unit/realtime/connection_spec.rb +1 -1
- data/spec/unit/realtime/presence_spec.rb +5 -5
- metadata +5 -5
data/lib/ably/modules/enum.rb
CHANGED
@@ -90,7 +90,13 @@ module Ably::Modules
|
|
90
90
|
end
|
91
91
|
|
92
92
|
private
|
93
|
-
|
93
|
+
def by_index
|
94
|
+
@by_index
|
95
|
+
end
|
96
|
+
|
97
|
+
def by_symbol
|
98
|
+
@by_symbol
|
99
|
+
end
|
94
100
|
|
95
101
|
# Define constants for each of the Enum values
|
96
102
|
# e.g. define_constants(:dog) creates Enum::Dog
|
@@ -173,7 +179,17 @@ module Ably::Modules
|
|
173
179
|
end
|
174
180
|
|
175
181
|
private
|
176
|
-
|
182
|
+
def name
|
183
|
+
@name
|
184
|
+
end
|
185
|
+
|
186
|
+
def index
|
187
|
+
@index
|
188
|
+
end
|
189
|
+
|
190
|
+
def symbol
|
191
|
+
@symbol
|
192
|
+
end
|
177
193
|
|
178
194
|
define_values values
|
179
195
|
end
|
@@ -39,7 +39,7 @@ module Ably
|
|
39
39
|
|
40
40
|
# Ensure @event_emitter_coerce_proc option is passed down to any classes that inherit the class with callbacks
|
41
41
|
def inherited(subclass)
|
42
|
-
subclass.instance_variable_set('@event_emitter_coerce_proc', @event_emitter_coerce_proc)
|
42
|
+
subclass.instance_variable_set('@event_emitter_coerce_proc', @event_emitter_coerce_proc) if defined?(@event_emitter_coerce_proc)
|
43
43
|
super
|
44
44
|
end
|
45
45
|
end
|
@@ -90,7 +90,7 @@ module Ably
|
|
90
90
|
clone.
|
91
91
|
select do |proc_hash|
|
92
92
|
if proc_hash[:unsafe]
|
93
|
-
proc_hash[:emit_proc].call
|
93
|
+
proc_hash[:emit_proc].call(*args)
|
94
94
|
else
|
95
95
|
safe_yield proc_hash[:emit_proc], *args
|
96
96
|
end
|
@@ -133,7 +133,7 @@ module Ably
|
|
133
133
|
def proc_for_block(block, options = {})
|
134
134
|
{
|
135
135
|
emit_proc: Proc.new do |*args|
|
136
|
-
block.call
|
136
|
+
block.call(*args)
|
137
137
|
true if options[:delete_once_run]
|
138
138
|
end,
|
139
139
|
block: block,
|
@@ -59,7 +59,7 @@ module Ably::Modules
|
|
59
59
|
|
60
60
|
private
|
61
61
|
def safe_deferrable_block(*args)
|
62
|
-
yield
|
62
|
+
yield(*args)
|
63
63
|
rescue StandardError => e
|
64
64
|
message = "An exception in a Deferrable callback was caught. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
65
65
|
if defined?(:logger) && logger.respond_to?(:error)
|
@@ -13,7 +13,7 @@ module Ably::Modules
|
|
13
13
|
private
|
14
14
|
|
15
15
|
def safe_yield(block, *args)
|
16
|
-
block.call
|
16
|
+
block.call(*args)
|
17
17
|
rescue StandardError => e
|
18
18
|
message = "An exception in an external block was caught. #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
|
19
19
|
safe_yield_log_error message
|
@@ -23,7 +23,7 @@ module Ably::Modules
|
|
23
23
|
if defined?(:logger) && logger.respond_to?(:error)
|
24
24
|
return logger.error message
|
25
25
|
end
|
26
|
-
rescue StandardError
|
26
|
+
rescue StandardError
|
27
27
|
fallback_logger.error message
|
28
28
|
end
|
29
29
|
|
@@ -82,21 +82,21 @@ module Ably::Modules
|
|
82
82
|
|
83
83
|
success_wrapper = Proc.new do
|
84
84
|
yield
|
85
|
-
off
|
86
|
-
off
|
85
|
+
off(&success_wrapper)
|
86
|
+
off(&failure_wrapper) if failure_wrapper
|
87
87
|
end
|
88
88
|
|
89
89
|
failure_wrapper = proc do |*args|
|
90
|
-
failure_block.call
|
91
|
-
off
|
92
|
-
off
|
90
|
+
failure_block.call(*args)
|
91
|
+
off(&success_wrapper)
|
92
|
+
off(&failure_wrapper)
|
93
93
|
end if failure_block
|
94
94
|
|
95
95
|
Array(target_states).each do |target_state|
|
96
96
|
safe_unsafe_method options[:unsafe], :once, target_state, &success_wrapper
|
97
97
|
|
98
98
|
safe_unsafe_method options[:unsafe], :once_state_changed do |*args|
|
99
|
-
failure_wrapper.call
|
99
|
+
failure_wrapper.call(*args) unless state == target_state
|
100
100
|
end if failure_block
|
101
101
|
end
|
102
102
|
end
|
@@ -119,8 +119,8 @@ module Ably::Modules
|
|
119
119
|
raise ArgumentError, 'Block required' unless block_given?
|
120
120
|
|
121
121
|
once_block = proc do |*args|
|
122
|
-
off
|
123
|
-
yield
|
122
|
+
off(*self.class::STATE.map, &once_block)
|
123
|
+
yield(*args)
|
124
124
|
end
|
125
125
|
|
126
126
|
safe_unsafe_method options[:unsafe], :once, *self.class::STATE.map, &once_block
|
@@ -5,7 +5,7 @@ module Ably::Modules
|
|
5
5
|
# This can be removed once https://github.com/gocardless/statesman/issues/95 is solved
|
6
6
|
def before_transition(options = nil, &block)
|
7
7
|
arrayify_transition(options) do |options_without_from_array|
|
8
|
-
super
|
8
|
+
super(*options_without_from_array, &block)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
@@ -13,7 +13,7 @@ module Ably::Modules
|
|
13
13
|
# This can be removed once https://github.com/gocardless/statesman/issues/95 is solved
|
14
14
|
def after_transition(options = nil, &block)
|
15
15
|
arrayify_transition(options) do |options_without_from_array|
|
16
|
-
super
|
16
|
+
super(*options_without_from_array, &block)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
@@ -59,7 +59,9 @@ module Ably::Modules
|
|
59
59
|
def_delegators :state_machine, :can_transition_to?
|
60
60
|
|
61
61
|
private
|
62
|
-
|
62
|
+
def state_machine
|
63
|
+
@state_machine
|
64
|
+
end
|
63
65
|
|
64
66
|
def_delegators :state_machine, :exception_for_state_change_to
|
65
67
|
|
@@ -85,7 +87,7 @@ module Ably::Modules
|
|
85
87
|
|
86
88
|
module ClassMethods
|
87
89
|
def emits_klass
|
88
|
-
@emits_klass ||= if @emits_klass_name
|
90
|
+
@emits_klass ||= if defined?(@emits_klass_name) && @emits_klass_name
|
89
91
|
get_const(@emits_klass_name)
|
90
92
|
end
|
91
93
|
end
|
data/lib/ably/realtime.rb
CHANGED
data/lib/ably/realtime/auth.rb
CHANGED
@@ -189,9 +189,13 @@ module Ably
|
|
189
189
|
private
|
190
190
|
# The synchronous Auth class instanced by the Rest client
|
191
191
|
# @return [Ably::Auth]
|
192
|
-
|
192
|
+
def auth_sync
|
193
|
+
@auth_sync
|
194
|
+
end
|
193
195
|
|
194
|
-
|
196
|
+
def client
|
197
|
+
@client
|
198
|
+
end
|
195
199
|
end
|
196
200
|
end
|
197
201
|
end
|
@@ -294,7 +294,9 @@ module Ably
|
|
294
294
|
private :change_state
|
295
295
|
|
296
296
|
private
|
297
|
-
|
297
|
+
def queue
|
298
|
+
@queue
|
299
|
+
end
|
298
300
|
|
299
301
|
def setup_event_handlers
|
300
302
|
__incoming_msgbus__.subscribe(:message) do |message|
|
@@ -323,7 +325,7 @@ module Ably
|
|
323
325
|
end
|
324
326
|
end
|
325
327
|
|
326
|
-
queue.push
|
328
|
+
queue.push(*messages)
|
327
329
|
|
328
330
|
if attached?
|
329
331
|
process_queue
|
@@ -385,8 +387,8 @@ module Ably
|
|
385
387
|
end
|
386
388
|
|
387
389
|
def create_message(message)
|
388
|
-
Ably::Models::Message(message.dup).tap do |
|
389
|
-
|
390
|
+
Ably::Models::Message(message.dup).tap do |msg|
|
391
|
+
msg.encode self
|
390
392
|
end
|
391
393
|
end
|
392
394
|
|
@@ -94,8 +94,14 @@ module Ably::Realtime
|
|
94
94
|
end
|
95
95
|
|
96
96
|
private
|
97
|
+
def channel
|
98
|
+
@channel
|
99
|
+
end
|
100
|
+
|
101
|
+
def connection
|
102
|
+
@connection
|
103
|
+
end
|
97
104
|
|
98
|
-
attr_reader :channel, :connection
|
99
105
|
def_delegators :channel, :can_transition_to?
|
100
106
|
|
101
107
|
# If the connection has not previously connected, connect now
|
data/lib/ably/realtime/client.rb
CHANGED
@@ -90,13 +90,13 @@ module Ably
|
|
90
90
|
@auth = Ably::Realtime::Auth.new(self)
|
91
91
|
@channels = Ably::Realtime::Channels.new(self)
|
92
92
|
@connection = Ably::Realtime::Connection.new(self, options)
|
93
|
-
@echo_messages =
|
94
|
-
@queue_messages =
|
95
|
-
@custom_realtime_host =
|
96
|
-
@auto_connect =
|
97
|
-
@recover =
|
93
|
+
@echo_messages = rest_client.options.fetch(:echo_messages, true) == false ? false : true
|
94
|
+
@queue_messages = rest_client.options.fetch(:queue_messages, true) == false ? false : true
|
95
|
+
@custom_realtime_host = rest_client.options[:realtime_host] || rest_client.options[:ws_host]
|
96
|
+
@auto_connect = rest_client.options.fetch(:auto_connect, true) == false ? false : true
|
97
|
+
@recover = rest_client.options[:recover]
|
98
98
|
|
99
|
-
raise ArgumentError, "Recovery key is invalid" if
|
99
|
+
raise ArgumentError, "Recovery key '#{recover}' is invalid" if recover && !recover.match(Connection::RECOVER_REGEX)
|
100
100
|
end
|
101
101
|
|
102
102
|
# Return a {Ably::Realtime::Channel Realtime Channel} for the given name
|
@@ -150,11 +150,6 @@ module Ably
|
|
150
150
|
endpoint_for_host(custom_realtime_host || [environment, DOMAIN].compact.join('-'))
|
151
151
|
end
|
152
152
|
|
153
|
-
|
154
|
-
def connection
|
155
|
-
@connection
|
156
|
-
end
|
157
|
-
|
158
153
|
# (see Ably::Rest::Client#register_encoder)
|
159
154
|
def register_encoder(encoder)
|
160
155
|
rest_client.register_encoder encoder
|
@@ -176,7 +171,7 @@ module Ably
|
|
176
171
|
# @return [URI::Generic] Fallback endpoint used to connect to the realtime Ably service. Note, after each connection attempt, a new random {Ably::FALLBACK_HOSTS fallback host} is used
|
177
172
|
# @api private
|
178
173
|
def fallback_endpoint
|
179
|
-
unless @fallback_endpoints
|
174
|
+
unless defined?(@fallback_endpoints) && @fallback_endpoints
|
180
175
|
@fallback_endpoints = Ably::FALLBACK_HOSTS.shuffle.map { |fallback_host| endpoint_for_host(fallback_host) }
|
181
176
|
end
|
182
177
|
|
@@ -13,7 +13,13 @@ module Ably::Realtime
|
|
13
13
|
end
|
14
14
|
|
15
15
|
private
|
16
|
-
|
16
|
+
def client
|
17
|
+
@client
|
18
|
+
end
|
19
|
+
|
20
|
+
def connection
|
21
|
+
@connection
|
22
|
+
end
|
17
23
|
|
18
24
|
def channels
|
19
25
|
client.channels
|
@@ -144,6 +150,7 @@ module Ably::Realtime
|
|
144
150
|
def process_connected_message(protocol_message)
|
145
151
|
if client.auth.token_client_id_allowed?(protocol_message.connection_details.client_id)
|
146
152
|
client.auth.configure_client_id protocol_message.connection_details.client_id
|
153
|
+
client.connection.set_connection_details protocol_message.connection_details
|
147
154
|
connection.transition_state_machine :connected, reason: protocol_message.error, protocol_message: protocol_message
|
148
155
|
else
|
149
156
|
reason = Ably::Exceptions::IncompatibleClientId.new("Client ID '#{protocol_message.connection_details.client_id}' specified by the server is incompatible with the library's configured client ID '#{client.client_id}'", 400, 40012)
|
@@ -195,7 +202,7 @@ module Ably::Realtime
|
|
195
202
|
|
196
203
|
def subscribe_to_incoming_protocol_messages
|
197
204
|
connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |*args|
|
198
|
-
dispatch_protocol_message
|
205
|
+
dispatch_protocol_message(*args)
|
199
206
|
end
|
200
207
|
end
|
201
208
|
end
|
@@ -58,7 +58,7 @@ module Ably
|
|
58
58
|
ensure_state_machine_emits 'Ably::Models::ConnectionStateChange'
|
59
59
|
|
60
60
|
# Expected format for a connection recover key
|
61
|
-
RECOVER_REGEX = /^(?<recover>[\w
|
61
|
+
RECOVER_REGEX = /^(?<recover>[\w!-]+):(?<connection_serial>\-?\w+)$/
|
62
62
|
|
63
63
|
# Defaults for automatic connection recovery and timeouts
|
64
64
|
DEFAULTS = {
|
@@ -84,6 +84,10 @@ module Ably
|
|
84
84
|
# @return [Ably::Models::ErrorInfo,Ably::Exceptions::BaseAblyException]
|
85
85
|
attr_reader :error_reason
|
86
86
|
|
87
|
+
# Connection details of the currently established connection
|
88
|
+
# @return [Ably::Models::ConnectionDetails]
|
89
|
+
attr_reader :details
|
90
|
+
|
87
91
|
# {Ably::Realtime::Client} associated with this connection
|
88
92
|
# @return [Ably::Realtime::Client]
|
89
93
|
attr_reader :client
|
@@ -178,12 +182,12 @@ module Ably
|
|
178
182
|
|
179
183
|
once(:connected) do
|
180
184
|
deferrable.succeed
|
181
|
-
off
|
185
|
+
off(&fail_callback)
|
182
186
|
end
|
183
187
|
|
184
188
|
once(:failed, :closed, :closing) do
|
185
189
|
deferrable.fail
|
186
|
-
off
|
190
|
+
off(&succeed_callback)
|
187
191
|
end
|
188
192
|
end
|
189
193
|
end
|
@@ -356,10 +360,10 @@ module Ably
|
|
356
360
|
# @api private
|
357
361
|
def send_protocol_message(protocol_message)
|
358
362
|
add_message_serial_if_ack_required_to(protocol_message) do
|
359
|
-
Ably::Models::ProtocolMessage.new(protocol_message, logger: logger).tap do |
|
360
|
-
add_message_to_outgoing_queue
|
361
|
-
notify_message_dispatcher_of_new_message
|
362
|
-
logger.debug("Connection: Prot msg queued =>: #{
|
363
|
+
Ably::Models::ProtocolMessage.new(protocol_message, logger: logger).tap do |message|
|
364
|
+
add_message_to_outgoing_queue message
|
365
|
+
notify_message_dispatcher_of_new_message message
|
366
|
+
logger.debug("Connection: Prot msg queued =>: #{message.action} #{message}")
|
363
367
|
end
|
364
368
|
end
|
365
369
|
end
|
@@ -437,6 +441,11 @@ module Ably
|
|
437
441
|
@error_reason = nil
|
438
442
|
end
|
439
443
|
|
444
|
+
# @api private
|
445
|
+
def set_connection_details(connection_details)
|
446
|
+
@details = connection_details
|
447
|
+
end
|
448
|
+
|
440
449
|
# Executes registered callbacks for a successful connection resume event
|
441
450
|
# @api private
|
442
451
|
def resumed
|
@@ -476,7 +485,9 @@ module Ably
|
|
476
485
|
# A connection serial guarantees the server has received the message and is thus used for connection
|
477
486
|
# recovery and resumes.
|
478
487
|
# @return [Integer] starting at -1 indicating no messages sent, 0 when the first message is sent
|
479
|
-
|
488
|
+
def client_serial
|
489
|
+
@client_serial
|
490
|
+
end
|
480
491
|
|
481
492
|
def resume_callbacks
|
482
493
|
@resume_callbacks ||= []
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'ably/rest/middleware/exceptions'
|
2
|
+
|
1
3
|
module Ably::Realtime
|
2
4
|
class Connection
|
3
5
|
# ConnectionManager is responsible for all actions relating to underlying connection and transports,
|
@@ -10,12 +12,13 @@ module Ably::Realtime
|
|
10
12
|
class ConnectionManager
|
11
13
|
# Error codes from the server that can potentially be resolved
|
12
14
|
RESOLVABLE_ERROR_CODES = {
|
13
|
-
token_expired:
|
15
|
+
token_expired: Ably::Rest::Middleware::Exceptions::TOKEN_EXPIRED_CODE
|
14
16
|
}
|
15
17
|
|
16
18
|
def initialize(connection)
|
17
|
-
@connection
|
18
|
-
@timers
|
19
|
+
@connection = connection
|
20
|
+
@timers = Hash.new { |hash, key| hash[key] = [] }
|
21
|
+
@renewing_token = false
|
19
22
|
|
20
23
|
connection.unsafe_on(:closed) do
|
21
24
|
connection.reset_resume_info
|
@@ -58,7 +61,7 @@ module Ably::Realtime
|
|
58
61
|
end
|
59
62
|
|
60
63
|
logger.debug "ConnectionManager: Setting up automatic connection timeout timer for #{realtime_request_timeout}s"
|
61
|
-
create_timeout_timer_whilst_in_state(:
|
64
|
+
create_timeout_timer_whilst_in_state(:connecting, realtime_request_timeout) do
|
62
65
|
connection_opening_failed Ably::Exceptions::ConnectionTimeout.new("Connection to Ably timed out after #{realtime_request_timeout}s", nil, 80014)
|
63
66
|
end
|
64
67
|
end
|
@@ -123,7 +126,7 @@ module Ably::Realtime
|
|
123
126
|
def close_connection
|
124
127
|
connection.send_protocol_message(action: Ably::Models::ProtocolMessage::ACTION.Close)
|
125
128
|
|
126
|
-
create_timeout_timer_whilst_in_state(:
|
129
|
+
create_timeout_timer_whilst_in_state(:closing, realtime_request_timeout) do
|
127
130
|
force_close_connection if connection.closing?
|
128
131
|
end
|
129
132
|
end
|
@@ -153,7 +156,7 @@ module Ably::Realtime
|
|
153
156
|
return unless can_retry_connection? # do not always reattempt connection or change state as client may be re-authorising
|
154
157
|
|
155
158
|
if error.kind_of?(Ably::Models::ErrorInfo)
|
156
|
-
if
|
159
|
+
if RESOLVABLE_ERROR_CODES.fetch(:token_expired).include?(error.code)
|
157
160
|
next_state = get_next_retry_state_info
|
158
161
|
logger.debug "ConnectionManager: Transport disconnected because of token expiry, pausing #{next_state.fetch(:pause)}s before reattempting to connect"
|
159
162
|
EventMachine.add_timer(next_state.fetch(:pause)) { renew_token_and_reconnect error }
|
@@ -181,7 +184,7 @@ module Ably::Realtime
|
|
181
184
|
logger.debug "ConnectionManager: Transport disconnected whilst connection in #{connection.state} state"
|
182
185
|
end
|
183
186
|
|
184
|
-
if error.kind_of?(Ably::Models::ErrorInfo) &&
|
187
|
+
if error.kind_of?(Ably::Models::ErrorInfo) && !RESOLVABLE_ERROR_CODES.fetch(:token_expired).include?(error.code)
|
185
188
|
connection.emit :error, error
|
186
189
|
logger.error "ConnectionManager: Error in Disconnected ProtocolMessage received from the server - #{error}"
|
187
190
|
end
|
@@ -213,11 +216,15 @@ module Ably::Realtime
|
|
213
216
|
end
|
214
217
|
|
215
218
|
private
|
216
|
-
|
219
|
+
def connection
|
220
|
+
@connection
|
221
|
+
end
|
217
222
|
|
218
223
|
# Timers used to manage connection state, for internal use by the client library
|
219
224
|
# @return [Hash]
|
220
|
-
|
225
|
+
def timers
|
226
|
+
@timers
|
227
|
+
end
|
221
228
|
|
222
229
|
def transport
|
223
230
|
connection.transport
|