ably 1.2.5 → 1.2.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +1 -1
  3. data/CHANGELOG.md +17 -0
  4. data/README.md +24 -7
  5. data/SPEC.md +1722 -853
  6. data/ably.gemspec +1 -1
  7. data/lib/ably/auth.rb +19 -11
  8. data/lib/ably/models/protocol_message.rb +5 -26
  9. data/lib/ably/modules/safe_deferrable.rb +2 -2
  10. data/lib/ably/modules/state_emitter.rb +1 -1
  11. data/lib/ably/realtime/auth.rb +4 -0
  12. data/lib/ably/realtime/channel/channel_manager.rb +51 -48
  13. data/lib/ably/realtime/channel/channel_properties.rb +9 -0
  14. data/lib/ably/realtime/channel/channel_state_machine.rb +2 -0
  15. data/lib/ably/realtime/channel.rb +4 -3
  16. data/lib/ably/realtime/channels.rb +20 -0
  17. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -13
  18. data/lib/ably/realtime/client.rb +14 -6
  19. data/lib/ably/realtime/connection/connection_manager.rb +21 -22
  20. data/lib/ably/realtime/connection.rb +77 -109
  21. data/lib/ably/realtime/presence/members_map.rb +41 -92
  22. data/lib/ably/realtime/presence/presence_manager.rb +12 -17
  23. data/lib/ably/realtime/presence.rb +15 -6
  24. data/lib/ably/realtime/push.rb +0 -27
  25. data/lib/ably/realtime/recovery_key_context.rb +36 -0
  26. data/lib/ably/rest/client.rb +4 -6
  27. data/lib/ably/rest/push/admin.rb +1 -1
  28. data/lib/ably/rest/push.rb +0 -19
  29. data/lib/ably/util/ably_extensions.rb +29 -0
  30. data/lib/ably/util/crypto.rb +2 -2
  31. data/lib/ably/util/safe_deferrable.rb +1 -1
  32. data/lib/ably/version.rb +5 -7
  33. data/spec/acceptance/realtime/channel_history_spec.rb +8 -12
  34. data/spec/acceptance/realtime/channel_spec.rb +474 -300
  35. data/spec/acceptance/realtime/client_spec.rb +1 -1
  36. data/spec/acceptance/realtime/connection_failures_spec.rb +8 -25
  37. data/spec/acceptance/realtime/connection_spec.rb +28 -120
  38. data/spec/acceptance/realtime/message_spec.rb +23 -52
  39. data/spec/acceptance/realtime/presence_spec.rb +123 -92
  40. data/spec/acceptance/rest/channel_spec.rb +2 -2
  41. data/spec/acceptance/rest/client_spec.rb +9 -2
  42. data/spec/acceptance/rest/message_spec.rb +8 -11
  43. data/spec/acceptance/rest/push_admin_spec.rb +20 -15
  44. data/spec/shared/client_initializer_behaviour.rb +1 -1
  45. data/spec/support/markdown_spec_formatter.rb +1 -1
  46. data/spec/unit/models/protocol_message_spec.rb +0 -78
  47. data/spec/unit/models/token_details_spec.rb +4 -2
  48. data/spec/unit/realtime/channels_spec.rb +1 -1
  49. data/spec/unit/realtime/connection_spec.rb +0 -30
  50. data/spec/unit/realtime/recovery_key_context_spec.rb +36 -0
  51. data/spec/unit/util/crypto_spec.rb +15 -15
  52. metadata +9 -9
  53. data/spec/acceptance/realtime/push_spec.rb +0 -27
  54. data/spec/acceptance/rest/push_spec.rb +0 -25
data/ably.gemspec CHANGED
@@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
38
38
  spec.add_development_dependency 'rspec-instafail', '~> 1.0'
39
39
  spec.add_development_dependency 'bundler', '>= 1.3.0'
40
40
  spec.add_development_dependency 'webmock', '~> 3.11'
41
- spec.add_development_dependency 'simplecov', '~> 0.21.2'
41
+ spec.add_development_dependency 'simplecov', '~> 0.22.0'
42
42
  spec.add_development_dependency 'simplecov-lcov', '~> 0.8.0'
43
43
  spec.add_development_dependency 'parallel_tests', '~> 3.8'
44
44
  spec.add_development_dependency 'pry', '~> 0.14.1'
data/lib/ably/auth.rb CHANGED
@@ -414,13 +414,20 @@ module Ably
414
414
  #
415
415
  # @return [Hash] headers
416
416
  def extra_auth_headers
417
- if client_id && using_basic_auth?
418
- { 'X-Ably-ClientId' => Base64.urlsafe_encode64(client_id) }
417
+ if client_id_for_request
418
+ { 'X-Ably-ClientId' => Base64.urlsafe_encode64(client_id_for_request) }
419
419
  else
420
420
  {}
421
421
  end
422
422
  end
423
423
 
424
+ # ClientId that needs to be included with every rest/realtime request
425
+ # spec - RSA7e
426
+ # @return string
427
+ def client_id_for_request
428
+ options[:client_id]
429
+ end
430
+
424
431
  # Auth params used in URI endpoint for Realtime connections
425
432
  # Will reauthorize implicitly if required and capable
426
433
  #
@@ -482,15 +489,16 @@ module Ably
482
489
  #
483
490
  # @api private
484
491
  def configure_client_id(new_client_id)
485
- # If new client ID from Ably is a wildcard, but preconfigured clientId is set, then keep the existing clientId
486
- if has_client_id? && new_client_id == '*'
487
- @client_id_validated = true
488
- return
489
- end
490
-
491
- # If client_id is defined and not a wildcard, prevent it changing, this is not supported
492
- if client_id && client_id != '*' && new_client_id != client_id
493
- raise Ably::Exceptions::IncompatibleClientId.new("Client ID is immutable once configured for a client. Client ID cannot be changed to '#{new_client_id}'")
492
+ if has_client_id?
493
+ # If new client ID from Ably is a wildcard, but preconfigured clientId is set, then keep the existing clientId
494
+ if new_client_id == "*"
495
+ @client_id_validated = true
496
+ return
497
+ end
498
+ # If client_id is defined and not a wildcard, prevent it changing, this is not supported
499
+ if new_client_id != client_id
500
+ raise Ably::Exceptions::IncompatibleClientId.new("Client ID is immutable once configured for a client. Client ID cannot be changed to '#{new_client_id}'")
501
+ end
494
502
  end
495
503
  @client_id_validated = true
496
504
  @client_id = new_client_id
@@ -20,8 +20,6 @@ module Ably::Models
20
20
  # @return [String] Contains a serial number for a message on the current channel
21
21
  # @!attribute [r] connection_id
22
22
  # @return [String] Contains a string private connection key used to recover this connection
23
- # @!attribute [r] connection_serial
24
- # @return [Bignum] Contains a serial number for a message sent from the server to the client
25
23
  # @!attribute [r] message_serial
26
24
  # @return [Bignum] Contains a serial number for a message sent from the client to the server
27
25
  # @!attribute [r] timestamp
@@ -129,12 +127,6 @@ module Ably::Models
129
127
  raise TypeError, "msg_serial '#{attributes[:msg_serial]}' is invalid, a positive Integer is expected for a ProtocolMessage"
130
128
  end
131
129
 
132
- def connection_serial
133
- Integer(attributes[:connection_serial])
134
- rescue TypeError
135
- raise TypeError, "connection_serial '#{attributes[:connection_serial]}' is invalid, a positive Integer is expected for a ProtocolMessage"
136
- end
137
-
138
130
  def count
139
131
  [1, attributes[:count].to_i].max
140
132
  end
@@ -146,26 +138,12 @@ module Ably::Models
146
138
  false
147
139
  end
148
140
 
149
- # @api private
150
- def has_connection_serial?
151
- connection_serial && true
141
+ def has_channel_serial?
142
+ channel_serial && true
152
143
  rescue TypeError
153
144
  false
154
145
  end
155
146
 
156
- def serial
157
- if has_connection_serial?
158
- connection_serial
159
- else
160
- message_serial
161
- end
162
- end
163
-
164
- # @api private
165
- def has_serial?
166
- has_connection_serial? || has_message_serial?
167
- end
168
-
169
147
  def messages
170
148
  @messages ||=
171
149
  Array(attributes[:messages]).map do |message|
@@ -271,7 +249,7 @@ module Ably::Models
271
249
  # Return a JSON ready object from the underlying #attributes using Ably naming conventions for keys
272
250
  def as_json(*args)
273
251
  raise TypeError, ':action is missing, cannot generate a valid Hash for ProtocolMessage' unless action
274
- raise TypeError, ':msg_serial or :connection_serial is missing, cannot generate a valid Hash for ProtocolMessage' if ack_required? && !has_serial?
252
+ raise TypeError, ':msg_serial is missing, cannot generate a valid Hash for ProtocolMessage' if ack_required? && !has_message_serial?
275
253
 
276
254
  attributes.dup.tap do |hash_object|
277
255
  hash_object['action'] = action.to_i
@@ -296,11 +274,12 @@ module Ably::Models
296
274
  end
297
275
 
298
276
  # True if the ProtocolMessage appears to be invalid, however this is not a guarantee
277
+ # Used for validating incoming protocol messages, so no need to add unnecessary checks
299
278
  # @return [Boolean]
300
279
  # @api private
301
280
  def invalid?
302
281
  action_enum = action rescue nil
303
- !action_enum || (ack_required? && !has_serial?)
282
+ !action_enum
304
283
  end
305
284
 
306
285
  # @!attribute [r] logger
@@ -39,7 +39,7 @@ module Ably::Modules
39
39
  end
40
40
  end
41
41
 
42
- # Mark the Deferrable as succeeded and trigger all callbacks.
42
+ # Mark the Deferrable as succeeded and trigger all success callbacks.
43
43
  # See http://www.rubydoc.info/gems/eventmachine/1.0.7/EventMachine/Deferrable#succeed-instance_method
44
44
  #
45
45
  # @return [void]
@@ -48,7 +48,7 @@ module Ably::Modules
48
48
  super(*args)
49
49
  end
50
50
 
51
- # Mark the Deferrable as failed and trigger all callbacks.
51
+ # Mark the Deferrable as failed and trigger all error callbacks.
52
52
  # See http://www.rubydoc.info/gems/eventmachine/1.0.7/EventMachine/Deferrable#fail-instance_method
53
53
  #
54
54
  # @return [void]
@@ -3,7 +3,7 @@ module Ably::Modules
3
3
  # the instance variable @state is used exclusively, the {Enum} STATE is defined prior to inclusion of this
4
4
  # module, and the class is an {EventEmitter}. It then emits state changes.
5
5
  #
6
- # It also ensures the EventEmitter is configured to retrict permitted events to the
6
+ # It also ensures the EventEmitter is configured to restrict permitted events to the
7
7
  # the available STATEs or EVENTs if defined i.e. if EVENTs includes an additional type such as
8
8
  # :update, then it will support all EVENTs being emitted. EVENTs must be a superset of STATEs
9
9
  #
@@ -226,6 +226,10 @@ module Ably
226
226
  auth_sync.auth_header
227
227
  end
228
228
 
229
+ def client_id_for_request_sync
230
+ auth_sync.client_id_for_request
231
+ end
232
+
229
233
  # Auth params used in URI endpoint for Realtime connections
230
234
  # Will reauthorize implicitly if required and capable
231
235
  #
@@ -18,7 +18,7 @@ module Ably::Realtime
18
18
  def attach
19
19
  if can_transition_to?(:attached)
20
20
  connect_if_connection_initialized
21
- send_attach_protocol_message
21
+ send_attach_protocol_message if connection.state?(:connected) # RTL4i
22
22
  end
23
23
  end
24
24
 
@@ -34,9 +34,9 @@ module Ably::Realtime
34
34
  # Channel is attached, notify presence if sync is expected
35
35
  def attached(attached_protocol_message)
36
36
  # If no attached ProtocolMessage then this attached request was triggered by the client
37
- # library, such as returning to attached whne detach has failed
37
+ # library, such as returning to attached when detach has failed
38
38
  if attached_protocol_message
39
- update_presence_sync_state_following_attached attached_protocol_message
39
+ channel.presence.manager.on_attach attached_protocol_message.has_presence_flag?
40
40
  channel.properties.set_attach_serial(attached_protocol_message.channel_serial)
41
41
  channel.options.set_modes_from_flags(attached_protocol_message.flags)
42
42
  channel.options.set_params(attached_protocol_message.params)
@@ -49,17 +49,16 @@ module Ably::Realtime
49
49
  end
50
50
 
51
51
  # Request channel to be reattached by sending an attach protocol message
52
- # @param [Hash] options
53
- # @option options [Ably::Models::ErrorInfo] :reason
54
- def request_reattach(options = {})
55
- reason = options[:reason]
56
- send_attach_protocol_message
57
- logger.debug { "Explicit channel reattach request sent to Ably due to #{reason}" }
52
+ # @param [Ably::Models::ErrorInfo] reason
53
+ def request_reattach(reason = nil)
58
54
  channel.set_channel_error_reason(reason) if reason
59
55
  channel.transition_state_machine! :attaching, reason: reason unless channel.attaching?
56
+ send_attach_protocol_message
57
+ logger.debug { "Explicit channel reattach request sent to Ably due to #{reason}" }
60
58
  end
61
59
 
62
60
  def duplicate_attached_received(protocol_message)
61
+ logger.debug { "Server initiated ATTACHED message received for channel '#{channel.name}' with state #{channel.state}" }
63
62
  if protocol_message.error
64
63
  channel.set_channel_error_reason protocol_message.error
65
64
  log_channel_error protocol_message.error
@@ -68,9 +67,7 @@ module Ably::Realtime
68
67
  channel.properties.set_attach_serial(protocol_message.channel_serial)
69
68
  channel.options.set_modes_from_flags(protocol_message.flags)
70
69
 
71
- if protocol_message.has_channel_resumed_flag?
72
- logger.debug { "ChannelManager: Additional resumed ATTACHED message received for #{channel.state} channel '#{channel.name}'" }
73
- else
70
+ unless protocol_message.has_channel_resumed_flag?
74
71
  channel.emit :update, Ably::Models::ChannelStateChange.new(
75
72
  current: channel.state,
76
73
  previous: channel.state,
@@ -78,7 +75,7 @@ module Ably::Realtime
78
75
  reason: protocol_message.error,
79
76
  resumed: false,
80
77
  )
81
- update_presence_sync_state_following_attached protocol_message
78
+ channel.presence.manager.on_attach protocol_message.has_presence_flag?
82
79
  end
83
80
  end
84
81
 
@@ -170,6 +167,12 @@ module Ably::Realtime
170
167
  end
171
168
  end
172
169
 
170
+ # RTL13c
171
+ def notify_state_change
172
+ @pending_state_change_timer.cancel if @pending_state_change_timer
173
+ @pending_state_change_timer = nil
174
+ end
175
+
173
176
  private
174
177
  attr_reader :pending_state_change_timer
175
178
 
@@ -209,56 +212,56 @@ module Ably::Realtime
209
212
  message_options[:flags] = message_options[:flags].to_i | Ably::Models::ProtocolMessage::ATTACH_FLAGS_MAPPING[:resume]
210
213
  end
211
214
 
212
- send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Attach, :suspended, message_options
213
- end
214
-
215
- def send_detach_protocol_message(previous_state)
216
- send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Detach, previous_state # return to previous state if failed
217
- end
215
+ message_options[:channelSerial] = channel.properties.channel_serial # RTL4c1
218
216
 
219
- def send_state_change_protocol_message(new_state, state_if_failed, message_options = {})
220
217
  state_at_time_of_request = channel.state
218
+ attach_action = Ably::Models::ProtocolMessage::ACTION.Attach
219
+ # RTL4f
221
220
  @pending_state_change_timer = EventMachine::Timer.new(realtime_request_timeout) do
222
221
  if channel.state == state_at_time_of_request
223
- error = Ably::Models::ErrorInfo.new(code: Ably::Exceptions::Codes::CHANNEL_OPERATION_FAILED_NO_RESPONSE_FROM_SERVER, message: "Channel #{new_state} operation failed (timed out)")
224
- channel.transition_state_machine state_if_failed, reason: error
222
+ error = Ably::Models::ErrorInfo.new(code: Ably::Exceptions::Codes::CHANNEL_OPERATION_FAILED_NO_RESPONSE_FROM_SERVER, message: "Channel #{attach_action} operation failed (timed out)")
223
+ channel.transition_state_machine :suspended, reason: error # return to suspended state if failed
225
224
  end
226
225
  end
226
+ # Shouldn't queue attach message as per RTL4i, so message is added top of the queue
227
+ # to be sent immediately while processing next message
228
+ connection.send_protocol_message_immediately(
229
+ action: attach_action.to_i,
230
+ channel: channel.name,
231
+ **message_options.to_h
232
+ )
233
+ end
227
234
 
228
- channel.once_state_changed do
229
- @pending_state_change_timer.cancel if @pending_state_change_timer
230
- @pending_state_change_timer = nil
235
+ def send_detach_protocol_message(previous_state)
236
+ state_at_time_of_request = channel.state
237
+ detach_action = Ably::Models::ProtocolMessage::ACTION.Detach
238
+
239
+ @pending_state_change_timer = EventMachine::Timer.new(realtime_request_timeout) do
240
+ if channel.state == state_at_time_of_request
241
+ error = Ably::Models::ErrorInfo.new(code: Ably::Exceptions::Codes::CHANNEL_OPERATION_FAILED_NO_RESPONSE_FROM_SERVER, message: "Channel #{detach_action} operation failed (timed out)")
242
+ channel.transition_state_machine previous_state, reason: error # return to previous state if failed
243
+ end
231
244
  end
232
245
 
233
- resend_if_disconnected_and_connected = lambda do
246
+ on_disconnected_and_connected = lambda do |&block|
234
247
  connection.unsafe_once(:disconnected) do
235
- next unless pending_state_change_timer
236
248
  connection.unsafe_once(:connected) do
237
- next unless pending_state_change_timer
238
- connection.send_protocol_message(
239
- action: new_state.to_i,
240
- channel: channel.name,
241
- **message_options.to_h
242
- )
243
- resend_if_disconnected_and_connected.call
244
- end
249
+ block.call if pending_state_change_timer
250
+ end if pending_state_change_timer
245
251
  end
246
252
  end
247
- resend_if_disconnected_and_connected.call
248
-
249
- connection.send_protocol_message(
250
- action: new_state.to_i,
251
- channel: channel.name,
252
- **message_options.to_h
253
- )
254
- end
255
253
 
256
- def update_presence_sync_state_following_attached(attached_protocol_message)
257
- if attached_protocol_message.has_presence_flag?
258
- channel.presence.manager.sync_expected
259
- else
260
- channel.presence.manager.sync_not_expected
254
+ send_detach_message = lambda do
255
+ on_disconnected_and_connected.call do
256
+ send_detach_message.call
257
+ end
258
+ connection.send_protocol_message(
259
+ action: detach_action.to_i,
260
+ channel: channel.name
261
+ )
261
262
  end
263
+
264
+ send_detach_message.call
262
265
  end
263
266
 
264
267
  def logger
@@ -18,6 +18,15 @@ module Ably::Realtime
18
18
  #
19
19
  attr_reader :attach_serial
20
20
 
21
+ # ChannelSerial contains the channelSerial from latest ProtocolMessage of action type
22
+ # Message/PresenceMessage received on the channel.
23
+ #
24
+ # @spec CP2b, RTL15b
25
+ #
26
+ # @return [String]
27
+ #
28
+ attr_accessor :channel_serial
29
+
21
30
  def initialize(channel)
22
31
  @channel = channel
23
32
  end
@@ -29,6 +29,7 @@ module Ably::Realtime
29
29
  transition :from => :failed, :to => [:attaching, :initialized]
30
30
 
31
31
  after_transition do |channel, transition|
32
+ channel.manager.notify_state_change # RTL13c
32
33
  channel.synchronize_state_with_statemachine
33
34
  end
34
35
 
@@ -55,6 +56,7 @@ module Ably::Realtime
55
56
  end
56
57
 
57
58
  after_transition(to: [:detached, :failed, :suspended]) do |channel, current_transition|
59
+ channel.properties.channel_serial = nil # RTP5a1
58
60
  err = error_from_state_change(current_transition)
59
61
  channel.manager.fail_queued_messages(err) if channel.failed? or channel.suspended? #RTL11
60
62
  channel.manager.log_channel_error err if err
@@ -42,7 +42,7 @@ module Ably
42
42
  #
43
43
  # @spec RTL2b
44
44
  #
45
- # The permited states for this channel
45
+ # The permitted states for this channel
46
46
  STATE = ruby_enum('STATE',
47
47
  :initialized,
48
48
  :attaching,
@@ -332,6 +332,7 @@ module Ably
332
332
  # @return [Ably::Util::SafeDeferrable]
333
333
  #
334
334
  def history(options = {}, &callback)
335
+ # RTL10b
335
336
  if options.delete(:until_attach)
336
337
  unless attached?
337
338
  error = Ably::Exceptions::InvalidRequest.new('option :until_attach is invalid as the channel is not attached' )
@@ -363,8 +364,8 @@ module Ably
363
364
  # @return [Ably::Models::ChannelOptions]
364
365
  def set_options(channel_options)
365
366
  @options = Ably::Models::ChannelOptions(channel_options)
366
-
367
- manager.request_reattach if need_reattach?
367
+ # RTL4i
368
+ manager.request_reattach if (need_reattach? and connection.state?(:connected))
368
369
  end
369
370
  alias options= set_options
370
371
 
@@ -46,6 +46,26 @@ module Ably
46
46
  @channels.delete(channel)
47
47
  end if @channels.has_key?(channel)
48
48
  end
49
+
50
+ # Sets channel serial to each channel from given serials hashmap
51
+ # @param [Hash] serials - map of channel name to respective channel serial
52
+ # @api private
53
+ def set_channel_serials(serials)
54
+ serials.each do |channel_name, channel_serial|
55
+ get(channel_name).properties.channel_serial = channel_serial
56
+ end
57
+ end
58
+
59
+ # @return [Hash] serials - map of channel name to respective channel serial
60
+ # @api private
61
+ def get_channel_serials
62
+ channel_serials = {}
63
+ self.each do |channel|
64
+ channel_serials[channel.name] = channel.properties.channel_serial if channel.state == :attached
65
+ end
66
+ channel_serials
67
+ end
68
+
49
69
  end
50
70
  end
51
71
  end
@@ -43,19 +43,24 @@ module Ably::Realtime
43
43
  raise ArgumentError, "Expected a ProtocolMessage. Received #{protocol_message}"
44
44
  end
45
45
 
46
- unless protocol_message.action.match_any?(:nack, :error)
47
- logger.debug { "#{protocol_message.action} received: #{protocol_message}" }
46
+ # RTL15b
47
+ if protocol_message.has_channel_serial? &&
48
+ (
49
+ protocol_message.action == :message ||
50
+ protocol_message.action == :presence ||
51
+ protocol_message.action == :attached
52
+ )
53
+ get_channel(protocol_message.channel).tap do |channel|
54
+ logger.info "Setting channel serial for channel #{channel.name}, " <<
55
+ "Previous: #{channel.properties.channel_serial}, New: #{protocol_message.channel_serial}"
56
+ channel.properties.channel_serial = protocol_message.channel_serial
57
+ end
48
58
  end
49
59
 
50
- if protocol_message.action.match_any?(:sync, :presence, :message)
51
- if connection.serial && protocol_message.has_connection_serial? && protocol_message.connection_serial <= connection.serial
52
- error_message = "Protocol error, duplicate message received for serial #{protocol_message.connection_serial}"
53
- logger.error error_message
54
- return
55
- end
60
+ unless protocol_message.action.match_any?(:nack, :error)
61
+ logger.debug { "#{protocol_message.action} received: #{protocol_message}" }
56
62
  end
57
63
 
58
- update_connection_recovery_info protocol_message
59
64
  connection.set_connection_confirmed_alive
60
65
 
61
66
  case protocol_message.action
@@ -172,10 +177,6 @@ module Ably::Realtime
172
177
  end
173
178
  end
174
179
 
175
- def update_connection_recovery_info(protocol_message)
176
- connection.update_connection_serial protocol_message.connection_serial if protocol_message.has_connection_serial?
177
- end
178
-
179
180
  def ack_pending_queue_for_message_serial(ack_protocol_message)
180
181
  drop_pending_queue_from_ack(ack_protocol_message) do |protocol_message|
181
182
  ack_messages protocol_message.messages
@@ -1,5 +1,6 @@
1
1
  require 'uri'
2
2
  require 'ably/realtime/channel/publisher'
3
+ require 'ably/realtime/recovery_key_context'
3
4
 
4
5
  module Ably
5
6
  module Realtime
@@ -11,6 +12,7 @@ module Ably
11
12
  include Ably::Modules::Conversions
12
13
 
13
14
  extend Forwardable
15
+ using Ably::Util::AblyExtensions
14
16
 
15
17
  DOMAIN = 'realtime.ably.io'
16
18
 
@@ -120,17 +122,23 @@ module Ably
120
122
  acc[key.to_s] = value.to_s
121
123
  end
122
124
  @rest_client = Ably::Rest::Client.new(options.merge(realtime_client: self))
123
- @echo_messages = rest_client.options.fetch(:echo_messages, true) == false ? false : true
124
- @queue_messages = rest_client.options.fetch(:queue_messages, true) == false ? false : true
125
+ @echo_messages = rest_client.options.fetch_with_default(:echo_messages, true)
126
+ @queue_messages = rest_client.options.fetch_with_default(:queue_messages, true)
125
127
  @custom_realtime_host = rest_client.options[:realtime_host] || rest_client.options[:ws_host]
126
- @auto_connect = rest_client.options.fetch(:auto_connect, true) == false ? false : true
127
- @recover = rest_client.options[:recover]
128
-
129
- raise ArgumentError, "Recovery key '#{recover}' is invalid" if recover && !recover.match(Connection::RECOVER_REGEX)
128
+ @auto_connect = rest_client.options.fetch_with_default(:auto_connect, true)
129
+ @recover = rest_client.options.fetch_with_default(:recover, '')
130
130
 
131
131
  @auth = Ably::Realtime::Auth.new(self)
132
132
  @channels = Ably::Realtime::Channels.new(self)
133
133
  @connection = Ably::Realtime::Connection.new(self, options)
134
+
135
+ unless @recover.nil_or_empty?
136
+ recovery_context = RecoveryKeyContext.from_json(@recover, logger)
137
+ unless recovery_context.nil?
138
+ @channels.set_channel_serials recovery_context.channel_serials # RTN16j
139
+ @connection.set_msg_serial_from_recover = recovery_context.msg_serial # RTN16f
140
+ end
141
+ end
134
142
  end
135
143
 
136
144
  # Return a {Ably::Realtime::Channel Realtime Channel} for the given name
@@ -14,12 +14,14 @@ module Ably::Realtime
14
14
  RESOLVABLE_ERROR_CODES = {
15
15
  token_expired: Ably::Exceptions::TOKEN_EXPIRED_CODE
16
16
  }
17
+ using Ably::Util::AblyExtensions
17
18
 
18
19
  def initialize(connection)
19
20
  @connection = connection
20
21
  @timers = Hash.new { |hash, key| hash[key] = [] }
21
22
 
22
- connection.unsafe_on(:closed) do
23
+ # RTN8c, RTN9c
24
+ connection.unsafe_on(:closing, :closed, :suspended, :failed) do
23
25
  connection.reset_resume_info
24
26
  end
25
27
 
@@ -111,23 +113,28 @@ module Ably::Realtime
111
113
  # Update the connection details and any associated defaults
112
114
  connection.set_connection_details protocol_message.connection_details
113
115
 
116
+ is_connection_resume_or_recover_attempt = !connection.key.nil_or_empty? || !client.recover.nil_or_empty?
117
+
118
+ # RTN15c7, RTN16d
119
+ failed_resume_or_recover = !protocol_message.connection_id == connection.id && !protocol_message.error.nil?
120
+ if is_connection_resume_or_recover_attempt and failed_resume_or_recover # RTN15c7
121
+ connection.reset_client_msg_serial
122
+ end
123
+ client.disable_automatic_connection_recovery # RTN16k, explicitly setting null, so it won't be used for subsequent connection requests
124
+
114
125
  if connection.key
115
126
  if protocol_message.connection_id == connection.id
116
127
  logger.debug { "ConnectionManager: Connection resumed successfully - ID #{connection.id} and key #{connection.key}" }
117
- EventMachine.next_tick { connection.trigger_resumed }
118
128
  resend_pending_message_ack_queue
119
129
  else
120
- logger.debug { "ConnectionManager: Connection was not resumed, old connection ID #{connection.id} has been updated with new connection ID #{protocol_message.connection_id} and key #{protocol_message.connection_details.connection_key}" }
121
130
  nack_messages_on_all_channels protocol_message.error
122
- force_reattach_on_channels protocol_message.error
123
131
  end
124
132
  else
125
133
  logger.debug { "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_details.connection_key}" }
126
134
  end
127
135
 
128
- reattach_suspended_channels protocol_message.error
129
-
130
- connection.configure_new protocol_message.connection_id, protocol_message.connection_details.connection_key, protocol_message.connection_serial
136
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_details.connection_key
137
+ force_reattach_on_channels protocol_message.error # irrespective of connection success/failure, reattach channels
131
138
  end
132
139
 
133
140
  # When connection is CONNECTED and receives an update
@@ -139,7 +146,7 @@ module Ably::Realtime
139
146
  # Update the connection details and any associated defaults
140
147
  connection.set_connection_details protocol_message.connection_details
141
148
 
142
- connection.configure_new protocol_message.connection_id, protocol_message.connection_details.connection_key, protocol_message.connection_serial
149
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_details.connection_key
143
150
 
144
151
  state_change = Ably::Models::ConnectionStateChange.new(
145
152
  current: connection.state,
@@ -281,7 +288,7 @@ module Ably::Realtime
281
288
  # Any message sent before an ACK/NACK was received on the previous transport
282
289
  # need to be resent to the Ably service so that a subsequent ACK/NACK is received.
283
290
  # It is up to Ably to ensure that duplicate messages are not retransmitted on the channel
284
- # base on the serial numbers
291
+ # based on the message serial numbers
285
292
  #
286
293
  # @api private
287
294
  def resend_pending_message_ack_queue
@@ -568,22 +575,14 @@ module Ably::Realtime
568
575
  client.auth.authorization_in_flight?
569
576
  end
570
577
 
571
- def reattach_suspended_channels(error)
572
- channels.select do |channel|
573
- channel.suspended?
574
- end.each do |channel|
575
- channel.transition_state_machine :attaching
576
- end
577
- end
578
-
579
- # When continuity on a connection is lost all messages
580
- # Channels in the ATTACHED or ATTACHING state should explicitly be re-attached
581
- # by sending a new ATTACH to Ably
578
+ # When reconnected if channel is in ATTACHING, ATTACHED or SUSPENDED state
579
+ # it should explicitly be re-attached by sending a new ATTACH to Ably
580
+ # Spec : RTN15c6, RTN15c7
582
581
  def force_reattach_on_channels(error)
583
582
  channels.select do |channel|
584
- channel.attached? || channel.attaching?
583
+ channel.attached? || channel.attaching? || channel.suspended?
585
584
  end.each do |channel|
586
- channel.manager.request_reattach reason: error
585
+ channel.manager.request_reattach error
587
586
  end
588
587
  end
589
588