ably 0.8.15 → 1.0.0

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.
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