ably 0.8.15 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -4
  3. data/CHANGELOG.md +6 -2
  4. data/README.md +5 -1
  5. data/SPEC.md +1473 -852
  6. data/ably.gemspec +11 -8
  7. data/lib/ably/auth.rb +90 -53
  8. data/lib/ably/exceptions.rb +37 -8
  9. data/lib/ably/logger.rb +10 -1
  10. data/lib/ably/models/auth_details.rb +42 -0
  11. data/lib/ably/models/channel_state_change.rb +18 -4
  12. data/lib/ably/models/connection_details.rb +6 -3
  13. data/lib/ably/models/connection_state_change.rb +4 -3
  14. data/lib/ably/models/error_info.rb +1 -1
  15. data/lib/ably/models/message.rb +17 -1
  16. data/lib/ably/models/message_encoders/base.rb +103 -82
  17. data/lib/ably/models/message_encoders/base64.rb +1 -1
  18. data/lib/ably/models/presence_message.rb +16 -1
  19. data/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/ably/models/token_details.rb +11 -1
  21. data/lib/ably/models/token_request.rb +16 -6
  22. data/lib/ably/modules/async_wrapper.rb +7 -3
  23. data/lib/ably/modules/encodeable.rb +51 -12
  24. data/lib/ably/modules/enum.rb +17 -7
  25. data/lib/ably/modules/event_emitter.rb +29 -14
  26. data/lib/ably/modules/model_common.rb +13 -21
  27. data/lib/ably/modules/state_emitter.rb +7 -4
  28. data/lib/ably/modules/state_machine.rb +2 -4
  29. data/lib/ably/modules/uses_state_machine.rb +7 -3
  30. data/lib/ably/realtime.rb +2 -0
  31. data/lib/ably/realtime/auth.rb +102 -42
  32. data/lib/ably/realtime/channel.rb +68 -26
  33. data/lib/ably/realtime/channel/channel_manager.rb +154 -65
  34. data/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  35. data/lib/ably/realtime/client.rb +18 -3
  36. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  37. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  38. data/lib/ably/realtime/connection.rb +108 -49
  39. data/lib/ably/realtime/connection/connection_manager.rb +167 -61
  40. data/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  41. data/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  42. data/lib/ably/realtime/presence.rb +70 -45
  43. data/lib/ably/realtime/presence/members_map.rb +201 -36
  44. data/lib/ably/realtime/presence/presence_manager.rb +30 -6
  45. data/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  46. data/lib/ably/rest.rb +2 -2
  47. data/lib/ably/rest/channel.rb +5 -5
  48. data/lib/ably/rest/client.rb +31 -27
  49. data/lib/ably/rest/middleware/exceptions.rb +1 -3
  50. data/lib/ably/rest/middleware/logger.rb +2 -2
  51. data/lib/ably/rest/presence.rb +2 -2
  52. data/lib/ably/util/pub_sub.rb +1 -1
  53. data/lib/ably/util/safe_deferrable.rb +26 -0
  54. data/lib/ably/version.rb +2 -2
  55. data/spec/acceptance/realtime/auth_spec.rb +470 -111
  56. data/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  57. data/spec/acceptance/realtime/channel_spec.rb +1017 -168
  58. data/spec/acceptance/realtime/client_spec.rb +6 -6
  59. data/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  60. data/spec/acceptance/realtime/connection_spec.rb +424 -105
  61. data/spec/acceptance/realtime/message_spec.rb +52 -23
  62. data/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  63. data/spec/acceptance/realtime/presence_spec.rb +1110 -96
  64. data/spec/acceptance/rest/auth_spec.rb +222 -59
  65. data/spec/acceptance/rest/base_spec.rb +1 -1
  66. data/spec/acceptance/rest/channel_spec.rb +1 -2
  67. data/spec/acceptance/rest/client_spec.rb +104 -48
  68. data/spec/acceptance/rest/message_spec.rb +42 -15
  69. data/spec/acceptance/rest/presence_spec.rb +4 -11
  70. data/spec/rspec_config.rb +2 -1
  71. data/spec/shared/client_initializer_behaviour.rb +2 -2
  72. data/spec/shared/safe_deferrable_behaviour.rb +6 -2
  73. data/spec/spec_helper.rb +4 -2
  74. data/spec/support/debug_failure_helper.rb +20 -4
  75. data/spec/support/event_machine_helper.rb +32 -1
  76. data/spec/unit/auth_spec.rb +4 -11
  77. data/spec/unit/logger_spec.rb +28 -2
  78. data/spec/unit/models/auth_details_spec.rb +49 -0
  79. data/spec/unit/models/channel_state_change_spec.rb +23 -3
  80. data/spec/unit/models/connection_details_spec.rb +12 -1
  81. data/spec/unit/models/connection_state_change_spec.rb +15 -4
  82. data/spec/unit/models/message_encoders/base64_spec.rb +2 -1
  83. data/spec/unit/models/message_spec.rb +153 -0
  84. data/spec/unit/models/presence_message_spec.rb +192 -0
  85. data/spec/unit/models/protocol_message_spec.rb +64 -6
  86. data/spec/unit/models/token_details_spec.rb +75 -0
  87. data/spec/unit/models/token_request_spec.rb +74 -0
  88. data/spec/unit/modules/async_wrapper_spec.rb +2 -1
  89. data/spec/unit/modules/enum_spec.rb +69 -0
  90. data/spec/unit/modules/event_emitter_spec.rb +149 -22
  91. data/spec/unit/modules/state_emitter_spec.rb +9 -3
  92. data/spec/unit/realtime/client_spec.rb +1 -1
  93. data/spec/unit/realtime/connection_spec.rb +8 -5
  94. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  95. data/spec/unit/realtime/presence_spec.rb +4 -3
  96. data/spec/unit/rest/client_spec.rb +1 -1
  97. data/spec/unit/util/crypto_spec.rb +3 -3
  98. metadata +22 -19
@@ -12,8 +12,6 @@ module Ably::Realtime
12
12
  def initialize(channel, connection)
13
13
  @channel = channel
14
14
  @connection = connection
15
-
16
- setup_connection_event_handlers
17
15
  end
18
16
 
19
17
  # Commence attachment
@@ -25,64 +23,130 @@ module Ably::Realtime
25
23
  end
26
24
 
27
25
  # Commence attachment
28
- def detach(error = nil)
26
+ def detach(error, previous_state)
29
27
  if connection.closed? || connection.connecting? || connection.suspended?
30
28
  channel.transition_state_machine :detached, reason: error
31
29
  elsif can_transition_to?(:detached)
32
- send_detach_protocol_message
30
+ send_detach_protocol_message previous_state
33
31
  end
34
32
  end
35
33
 
36
34
  # Channel is attached, notify presence if sync is expected
37
35
  def attached(attached_protocol_message)
38
- if attached_protocol_message.has_presence_flag?
39
- channel.presence.manager.sync_expected
40
- else
41
- channel.presence.manager.sync_not_expected
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
38
+ if attached_protocol_message
39
+ update_presence_sync_state_following_attached attached_protocol_message
40
+ channel.set_attached_serial attached_protocol_message.channel_serial
42
41
  end
43
- channel.set_attached_serial attached_protocol_message.channel_serial
44
42
  end
45
43
 
46
44
  # An error has occurred on the channel
47
- def emit_error(error)
48
- logger.error "ChannelManager: Channel '#{channel.name}' error: #{error}"
49
- channel.emit :error, error
45
+ def log_channel_error(error)
46
+ logger.error { "ChannelManager: Channel '#{channel.name}' error: #{error}" }
50
47
  end
51
48
 
52
- # Detach a channel as a result of an error
53
- def suspend(error)
54
- channel.transition_state_machine! :detaching, reason: error
49
+ # Request channel to be reattached by sending an attach protocol message
50
+ # @param [Hash] options
51
+ # @option options [Ably::Models::ErrorInfo] :reason
52
+ def request_reattach(options = {})
53
+ reason = options[:reason]
54
+ send_attach_protocol_message
55
+ logger.debug { "Explicit channel reattach request sent to Ably due to #{reason}" }
56
+ channel.set_channel_error_reason(reason) if reason
57
+ channel.transition_state_machine! :attaching, reason: reason unless channel.attaching?
55
58
  end
56
59
 
57
- # When a channel is no longer attached or has failed,
58
- # all messages awaiting an ACK response should fail immediately
59
- def fail_messages_awaiting_ack(error)
60
- # Allow a short time for other queued operations to complete before failing all messages
61
- EventMachine.next_tick do
62
- error = Ably::Exceptions::MessageDeliveryFailed.new("Channel cannot publish messages whilst state is '#{channel.state}'") unless error
60
+ def duplicate_attached_received(protocol_message)
61
+ if protocol_message.error
62
+ channel.set_channel_error_reason protocol_message.error
63
+ log_channel_error protocol_message.error
64
+ end
65
+
66
+ if protocol_message.has_channel_resumed_flag?
67
+ logger.debug { "ChannelManager: Additional resumed ATTACHED message received for #{channel.state} channel '#{channel.name}'" }
68
+ else
69
+ channel.emit :update, Ably::Models::ChannelStateChange.new(
70
+ current: channel.state,
71
+ previous: channel.state,
72
+ event: Ably::Realtime::Channel::EVENT(:update),
73
+ reason: protocol_message.error,
74
+ resumed: false,
75
+ )
76
+ update_presence_sync_state_following_attached protocol_message
77
+ end
78
+
79
+ channel.set_attached_serial protocol_message.channel_serial
80
+ end
81
+
82
+ # Handle DETACED messages, see #RTL13 for server-initated detaches
83
+ def detached_received(reason)
84
+ case channel.state.to_sym
85
+ when :detaching
86
+ channel.transition_state_machine :detached, reason: reason
87
+ when :attached, :suspended
88
+ channel.transition_state_machine :attaching, reason: reason
89
+ else
90
+ logger.debug { "ChannelManager: DETACHED ProtocolMessage received, but no action to take as not DETACHING, ATTACHED OR SUSPENDED" }
91
+ end
92
+ end
93
+
94
+ # When continuity on the connection is interrupted or channel becomes suspended (implying loss of continuity)
95
+ # then all messages published but awaiting an ACK from Ably should be failed with a NACK
96
+ # @param [Hash] options
97
+ # @option options [Boolean] :immediately
98
+ def fail_messages_awaiting_ack(error, options = {})
99
+ immediately = options[:immediately] || false
100
+
101
+ fail_proc = Proc.new do
102
+ error = Ably::Exceptions::MessageDeliveryFailed.new("Continuity of connection was lost so published messages awaiting ACK have failed") unless error
63
103
  fail_messages_in_queue connection.__pending_message_ack_queue__, error
64
- fail_messages_in_queue connection.__outgoing_message_queue__, error
65
104
  end
105
+
106
+ # Allow a short time for other queued operations to complete before failing all messages
107
+ if immediately
108
+ fail_proc.call
109
+ else
110
+ EventMachine.add_timer(0.1) { fail_proc.call }
111
+ end
112
+ end
113
+
114
+ # When a channel becomes detached, suspended or failed,
115
+ # all queued messages should be failed immediately as we don't queue in
116
+ # any of those states
117
+ def fail_queued_messages(error)
118
+ error = Ably::Exceptions::MessageDeliveryFailed.new("Queued messages on channel '#{channel.name}' in state '#{channel.state}' will never be delivered") unless error
119
+ fail_messages_in_queue connection.__outgoing_message_queue__, error
120
+ channel.__queue__.each do |message|
121
+ nack_message message, error
122
+ end
123
+ channel.__queue__.clear
66
124
  end
67
125
 
68
126
  def fail_messages_in_queue(queue, error)
69
127
  queue.delete_if do |protocol_message|
70
- if protocol_message.channel == channel.name
71
- nack_messages protocol_message, error
72
- true
128
+ if protocol_message.action.match_any?(:presence, :message)
129
+ if protocol_message.channel == channel.name
130
+ nack_messages protocol_message, error
131
+ true
132
+ end
73
133
  end
74
134
  end
75
135
  end
76
136
 
77
137
  def nack_messages(protocol_message, error)
78
138
  (protocol_message.messages + protocol_message.presence).each do |message|
79
- logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_safe_json}, protocol message: #{protocol_message}"
80
- message.fail error
139
+ nack_message message, error, protocol_message
81
140
  end
82
- logger.debug "Calling NACK failure callbacks for #{protocol_message.class.name} - #{protocol_message.to_safe_json}"
141
+ logger.debug { "Calling NACK failure callbacks for #{protocol_message.class.name} - #{protocol_message.to_json}" }
83
142
  protocol_message.fail error
84
143
  end
85
144
 
145
+ def nack_message(message, error, protocol_message = nil)
146
+ logger.debug { "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json} #{"protocol message: #{protocol_message}" if protocol_message}" }
147
+ message.fail error
148
+ end
149
+
86
150
  def drop_pending_queue_from_ack(ack_protocol_message)
87
151
  message_serial_up_to = ack_protocol_message.message_serial + ack_protocol_message.count - 1
88
152
  connection.__pending_message_ack_queue__.drop_while do |protocol_message|
@@ -93,7 +157,23 @@ module Ably::Realtime
93
157
  end
94
158
  end
95
159
 
160
+ # If the connection is still connected and the channel still suspended after
161
+ # channel_retry_timeout has passed, then attempt to reattach automatically, see #RTL13b
162
+ def start_attach_from_suspended_timer
163
+ cancel_attach_from_suspended_timer
164
+ if connection.connected?
165
+ channel.unsafe_once { |event| cancel_attach_from_suspended_timer unless event == :update }
166
+ connection.unsafe_once { |event| cancel_attach_from_suspended_timer unless event == :update }
167
+
168
+ @attach_from_suspended_timer = EventMachine::Timer.new(channel_retry_timeout) do
169
+ channel.transition_state_machine! :attaching
170
+ end
171
+ end
172
+ end
173
+
96
174
  private
175
+ attr_reader :pending_state_change_timer
176
+
97
177
  def channel
98
178
  @channel
99
179
  end
@@ -104,63 +184,72 @@ module Ably::Realtime
104
184
 
105
185
  def_delegators :channel, :can_transition_to?
106
186
 
187
+ def cancel_attach_from_suspended_timer
188
+ @attach_from_suspended_timer.cancel if @attach_from_suspended_timer
189
+ @attach_from_suspended_timer = nil
190
+ end
191
+
107
192
  # If the connection has not previously connected, connect now
108
193
  def connect_if_connection_initialized
109
194
  connection.connect if connection.initialized?
110
195
  end
111
196
 
112
- def send_attach_protocol_message
113
- send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Attach
197
+ def realtime_request_timeout
198
+ connection.defaults.fetch(:realtime_request_timeout)
114
199
  end
115
200
 
116
- def send_detach_protocol_message
117
- send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Detach
201
+ def channel_retry_timeout
202
+ connection.defaults.fetch(:channel_retry_timeout)
118
203
  end
119
204
 
120
- def send_state_change_protocol_message(state)
121
- connection.send_protocol_message(
122
- action: state.to_i,
123
- channel: channel.name
124
- )
205
+ def send_attach_protocol_message
206
+ send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Attach, :suspended # move to suspended
125
207
  end
126
208
 
127
- # Any message sent before an ACK/NACK was received on the previous transport
128
- # needs to be resent to the Ably service so that a subsequent ACK/NACK is received.
129
- # It is up to Ably to ensure that duplicate messages are not retransmitted on the channel
130
- # base on the serial numbers
131
- #
132
- # TODO: Move this into the Connection class, it does not belong in a Channel class
133
- #
134
- # @api private
135
- def resend_pending_message_ack_queue
136
- connection.__pending_message_ack_queue__.delete_if do |protocol_message|
137
- if protocol_message.channel == channel.name
138
- connection.__outgoing_message_queue__ << protocol_message
139
- connection.__outgoing_protocol_msgbus__.publish :protocol_message
140
- true
141
- end
142
- end
209
+ def send_detach_protocol_message(previous_state)
210
+ send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Detach, previous_state # return to previous state if failed
143
211
  end
144
212
 
145
- def setup_connection_event_handlers
146
- connection.unsafe_on(:closed) do
147
- channel.transition_state_machine :detaching if can_transition_to?(:detaching)
213
+ def send_state_change_protocol_message(new_state, state_if_failed)
214
+ state_at_time_of_request = channel.state
215
+ @pending_state_change_timer = EventMachine::Timer.new(realtime_request_timeout) do
216
+ if channel.state == state_at_time_of_request
217
+ error = Ably::Models::ErrorInfo.new(code: 90007, message: "Channel #{new_state} operation failed (timed out)")
218
+ channel.transition_state_machine state_if_failed, reason: error
219
+ end
148
220
  end
149
221
 
150
- connection.unsafe_on(:suspended) do |error|
151
- if can_transition_to?(:detaching)
152
- channel.transition_state_machine :detaching, reason: Ably::Exceptions::ConnectionSuspended.new('Connection suspended', nil, 80002, error)
153
- end
222
+ channel.once_state_changed do
223
+ @pending_state_change_timer.cancel if @pending_state_change_timer
224
+ @pending_state_change_timer = nil
154
225
  end
155
226
 
156
- connection.unsafe_on(:failed) do |error|
157
- if can_transition_to?(:failed) && !channel.detached?
158
- channel.transition_state_machine :failed, reason: Ably::Exceptions::ConnectionFailed.new('Connection failed', nil, 80002, error)
227
+ resend_if_disconnected_and_connected = Proc.new do
228
+ connection.unsafe_once(:disconnected) do
229
+ next unless pending_state_change_timer
230
+ connection.unsafe_once(:connected) do
231
+ next unless pending_state_change_timer
232
+ connection.send_protocol_message(
233
+ action: new_state.to_i,
234
+ channel: channel.name
235
+ )
236
+ resend_if_disconnected_and_connected.call
237
+ end
159
238
  end
160
239
  end
240
+ resend_if_disconnected_and_connected.call
161
241
 
162
- connection.unsafe_on(:connected) do |error|
163
- resend_pending_message_ack_queue
242
+ connection.send_protocol_message(
243
+ action: new_state.to_i,
244
+ channel: channel.name
245
+ )
246
+ end
247
+
248
+ def update_presence_sync_state_following_attached(attached_protocol_message)
249
+ if attached_protocol_message.has_presence_flag?
250
+ channel.presence.manager.sync_expected
251
+ else
252
+ channel.presence.manager.sync_not_expected
164
253
  end
165
254
  end
166
255
 
@@ -20,11 +20,12 @@ module Ably::Realtime
20
20
  state state_enum.to_sym, initial: index == 0
21
21
  end
22
22
 
23
- transition :from => :initialized, :to => [:attaching]
24
- transition :from => :attaching, :to => [:attached, :detaching, :failed]
25
- transition :from => :attached, :to => [:detaching, :detached, :failed]
26
- transition :from => :detaching, :to => [:detached, :attaching, :failed]
23
+ transition :from => :initialized, :to => [:attaching, :failed]
24
+ transition :from => :attaching, :to => [:attached, :detaching, :failed, :suspended]
25
+ transition :from => :attached, :to => [:attaching, :detaching, :detached, :failed, :suspended]
26
+ transition :from => :detaching, :to => [:detached, :attaching, :attached, :failed, :suspended]
27
27
  transition :from => :detached, :to => [:attaching, :attached, :failed]
28
+ transition :from => :suspended, :to => [:attaching, :detached, :failed]
28
29
  transition :from => :failed, :to => [:attaching]
29
30
 
30
31
  after_transition do |channel, transition|
@@ -41,31 +42,29 @@ module Ably::Realtime
41
42
 
42
43
  after_transition(to: [:detaching]) do |channel, current_transition|
43
44
  err = error_from_state_change(current_transition)
44
- channel.manager.detach err
45
+ channel.manager.detach err, current_transition.metadata.previous
45
46
  end
46
47
 
47
- after_transition(to: [:detached]) do |channel, current_transition|
48
+ after_transition(to: [:detached, :failed, :suspended]) do |channel, current_transition|
48
49
  err = error_from_state_change(current_transition)
49
- channel.manager.fail_messages_awaiting_ack err
50
- channel.manager.emit_error err if err
50
+ channel.manager.fail_queued_messages err
51
+ channel.manager.log_channel_error err if err
51
52
  end
52
53
 
53
- after_transition(to: [:failed]) do |channel, current_transition|
54
- err = error_from_state_change(current_transition)
55
- channel.manager.fail_messages_awaiting_ack err
56
- channel.manager.emit_error err if err
54
+ after_transition(to: [:suspended]) do |channel, current_transition|
55
+ channel.manager.start_attach_from_suspended_timer
57
56
  end
58
57
 
59
58
  # Transitions responsible for updating channel#error_reason
60
- before_transition(to: [:failed]) do |channel, current_transition|
59
+ before_transition(to: [:failed, :suspended]) do |channel, current_transition|
61
60
  err = error_from_state_change(current_transition)
62
- channel.set_failed_channel_error_reason err if err
61
+ channel.set_channel_error_reason err if err
63
62
  end
64
63
 
65
64
  before_transition(to: [:attached, :detached]) do |channel, current_transition|
66
65
  err = error_from_state_change(current_transition)
67
66
  if err
68
- channel.set_failed_channel_error_reason err
67
+ channel.set_channel_error_reason err
69
68
  else
70
69
  # Attached & Detached are "healthy" final states so reset the error reason
71
70
  channel.clear_error_reason
@@ -1,3 +1,5 @@
1
+ require 'uri'
2
+
1
3
  module Ably
2
4
  module Realtime
3
5
  # Client for the Ably Realtime API
@@ -76,8 +78,10 @@ module Ably
76
78
  # @option options [String] :recover When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery
77
79
  # @option options [Boolean] :auto_connect By default as soon as the client library is instantiated it will connect to Ably. You can optionally set this to false and explicitly connect.
78
80
  #
79
- # @option options [Integer] :disconnected_retry_timeout (15 seconds). When the connection enters the DISCONNECTED state, after this delay in milliseconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
80
- # @option options [Integer] :suspended_retry_timeout (30 seconds). When the connection enters the SUSPENDED state, after this delay in milliseconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically
81
+ # @option options [Integer] :channel_retry_timeout (15 seconds). When a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED
82
+ # @option options [Integer] :disconnected_retry_timeout (15 seconds). When the connection enters the DISCONNECTED state, after this delay in seconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
83
+ # @option options [Integer] :suspended_retry_timeout (30 seconds). When the connection enters the SUSPENDED state, after this delay in seconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically
84
+ # @option options [Boolean] :disable_websocket_heartbeats WebSocket heartbeats are more efficient than protocol level heartbeats, however they can be disabled for development purposes
81
85
  #
82
86
  # @return [Ably::Realtime::Client]
83
87
  #
@@ -89,7 +93,18 @@ module Ably
89
93
  # client = Ably::Realtime::Client.new(key: 'key.id:secret', client_id: 'john')
90
94
  #
91
95
  def initialize(options)
92
- @rest_client = Ably::Rest::Client.new(options)
96
+ raise ArgumentError, 'Options Hash is expected' if options.nil?
97
+
98
+ options = options.clone
99
+ if options.kind_of?(String)
100
+ options = if options.match(Ably::Auth::API_KEY_REGEX)
101
+ { key: options }
102
+ else
103
+ { token: options }
104
+ end
105
+ end
106
+
107
+ @rest_client = Ably::Rest::Client.new(options.merge(realtime_client: self))
93
108
  @auth = Ably::Realtime::Auth.new(self)
94
109
  @channels = Ably::Realtime::Channels.new(self)
95
110
  @connection = Ably::Realtime::Connection.new(self, options)
@@ -27,7 +27,7 @@ module Ably::Realtime
27
27
 
28
28
  def get_channel(channel_name)
29
29
  channels.fetch(channel_name) do
30
- logger.warn "Received channel message for non-existent channel"
30
+ logger.warn { "Received channel message for non-existent channel" }
31
31
  Ably::Realtime::Models::NilChannel.new
32
32
  end
33
33
  end
@@ -43,19 +43,13 @@ module Ably::Realtime
43
43
  raise ArgumentError, "Expected a ProtocolMessage. Received #{protocol_message}"
44
44
  end
45
45
 
46
- unless [:nack, :error].include?(protocol_message.action)
47
- logger.debug "#{protocol_message.action} received: #{protocol_message}"
46
+ unless protocol_message.action.match_any?(:nack, :error)
47
+ logger.debug { "#{protocol_message.action} received: #{protocol_message}" }
48
48
  end
49
49
 
50
- if [:sync, :presence, :message].any? { |prevent_duplicate| protocol_message.action == prevent_duplicate }
50
+ if protocol_message.action.match_any?(:sync, :presence, :message)
51
51
  if connection.serial && protocol_message.has_connection_serial? && protocol_message.connection_serial <= connection.serial
52
- error_target = if protocol_message.channel
53
- get_channel(protocol_message.channel)
54
- else
55
- connection
56
- end
57
52
  error_message = "Protocol error, duplicate message received for serial #{protocol_message.connection_serial}"
58
- error_target.emit :error, Ably::Exceptions::ProtocolError.new(error_message, 400, 80013)
59
53
  logger.error error_message
60
54
  return
61
55
  end
@@ -69,15 +63,18 @@ module Ably::Realtime
69
63
  ack_pending_queue_for_message_serial(protocol_message) if protocol_message.has_message_serial?
70
64
 
71
65
  when ACTION.Nack
72
- logger.warn "NACK received: #{protocol_message}"
66
+ logger.warn { "NACK received: #{protocol_message}" }
73
67
  nack_pending_queue_for_message_serial(protocol_message) if protocol_message.has_message_serial?
74
68
 
75
69
  when ACTION.Connect
76
70
  when ACTION.Connected
77
- if connection.disconnected? || connection.closing? || connection.closed? || connection.failed?
78
- logger.debug "Incoming CONNECTED ProtocolMessage discarded as connection has moved on and is in state: #{connection.state}"
71
+ if connection.closing?
72
+ logger.debug { "Out-of-order incoming CONNECTED ProtocolMessage discarded as connection has moved on and is in state: #{connection.state}" }
73
+ elsif connection.disconnected? || connection.closing? || connection.closed? || connection.failed?
74
+ logger.warn { "Out-of-order incoming CONNECTED ProtocolMessage discarded as connection has moved on and is in state: #{connection.state}" }
79
75
  elsif connection.connected?
80
- logger.error "CONNECTED ProtocolMessage should not have been received when the connection is in the CONNECTED state"
76
+ logger.debug { "Updated CONNECTED ProtocolMessage received (whilst connected)" }
77
+ process_connected_update_message protocol_message
81
78
  else
82
79
  process_connected_message protocol_message
83
80
  end
@@ -90,7 +87,7 @@ module Ably::Realtime
90
87
  connection.transition_state_machine :closed unless connection.closed?
91
88
 
92
89
  when ACTION.Error
93
- if protocol_message.channel && !protocol_message.has_message_serial?
90
+ if protocol_message.channel
94
91
  dispatch_channel_error protocol_message
95
92
  else
96
93
  process_connection_error protocol_message
@@ -99,21 +96,22 @@ module Ably::Realtime
99
96
  when ACTION.Attach
100
97
  when ACTION.Attached
101
98
  get_channel(protocol_message.channel).tap do |channel|
102
- channel.transition_state_machine :attached, reason: protocol_message.error, protocol_message: protocol_message unless channel.attached?
99
+ if channel.attached?
100
+ channel.manager.duplicate_attached_received protocol_message
101
+ else
102
+ channel.transition_state_machine :attached, reason: protocol_message.error, resumed: protocol_message.has_channel_resumed_flag?, protocol_message: protocol_message
103
+ end
103
104
  end
104
105
 
105
106
  when ACTION.Detach
106
107
  when ACTION.Detached
107
108
  get_channel(protocol_message.channel).tap do |channel|
108
- channel.transition_state_machine :detached unless channel.detached?
109
+ channel.manager.detached_received protocol_message.error
109
110
  end
110
111
 
111
112
  when ACTION.Sync
112
113
  presence = get_channel(protocol_message.channel).presence
113
- protocol_message.presence.each do |presence_message|
114
- presence.__incoming_msgbus__.publish :sync, presence_message
115
- end
116
- presence.members.update_sync_serial protocol_message.channel_serial
114
+ presence.manager.sync_process_messages protocol_message.channel_serial, protocol_message.presence
117
115
 
118
116
  when ACTION.Presence
119
117
  presence = get_channel(protocol_message.channel).presence
@@ -127,19 +125,23 @@ module Ably::Realtime
127
125
  channel.__incoming_msgbus__.publish :message, message
128
126
  end
129
127
 
128
+ when ACTION.Auth
129
+ client.auth.authorize
130
+
130
131
  else
131
132
  error = Ably::Exceptions::ProtocolError.new("Protocol Message Action #{protocol_message.action} is unsupported by this MessageDispatcher", 400, 80013)
132
- client.connection.emit :error, error
133
133
  logger.fatal error.message
134
134
  end
135
+
136
+ connection.set_connection_confirmed_alive
135
137
  end
136
138
 
137
139
  def dispatch_channel_error(protocol_message)
138
- logger.warn "Channel Error message received: #{protocol_message.error}"
140
+ logger.warn { "Channel Error message received: #{protocol_message.error}" }
139
141
  if !protocol_message.has_message_serial?
140
142
  get_channel(protocol_message.channel).transition_state_machine :failed, reason: protocol_message.error
141
143
  else
142
- logger.fatal "Cannot process ProtocolMessage as not yet implemented: #{protocol_message}"
144
+ logger.fatal { "Cannot process ProtocolMessage ERROR with message serial as not yet implemented: #{protocol_message}" }
143
145
  end
144
146
  end
145
147
 
@@ -149,11 +151,18 @@ module Ably::Realtime
149
151
 
150
152
  def process_connected_message(protocol_message)
151
153
  if client.auth.token_client_id_allowed?(protocol_message.connection_details.client_id)
152
- client.auth.configure_client_id protocol_message.connection_details.client_id
153
- client.connection.set_connection_details protocol_message.connection_details
154
154
  connection.transition_state_machine :connected, reason: protocol_message.error, protocol_message: protocol_message
155
155
  else
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)
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}'")
157
+ connection.transition_state_machine :failed, reason: reason, protocol_message: protocol_message
158
+ end
159
+ end
160
+
161
+ def process_connected_update_message(protocol_message)
162
+ if client.auth.token_client_id_allowed?(protocol_message.connection_details.client_id)
163
+ connection.manager.connected_update protocol_message
164
+ else
165
+ reason = Ably::Exceptions::IncompatibleClientId.new("Client ID '#{protocol_message.connection_details.client_id}' in CONNECTED update specified by the server is incompatible with the library's configured client ID '#{client.client_id}'")
157
166
  connection.transition_state_machine :failed, reason: reason, protocol_message: protocol_message
158
167
  end
159
168
  end
@@ -178,14 +187,14 @@ module Ably::Realtime
178
187
 
179
188
  def ack_messages(messages)
180
189
  messages.each do |message|
181
- logger.debug "Calling ACK success callbacks for #{message.class.name} - #{message.to_safe_json}"
190
+ logger.debug { "Calling ACK success callbacks for #{message.class.name} - #{message.to_json}" }
182
191
  message.succeed message
183
192
  end
184
193
  end
185
194
 
186
195
  def nack_messages(messages, protocol_message)
187
196
  messages.each do |message|
188
- logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_safe_json}, protocol message: #{protocol_message}"
197
+ logger.debug { "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}" }
189
198
  message.fail protocol_message.error
190
199
  end
191
200
  end