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
@@ -47,8 +47,7 @@ module Ably::Realtime
47
47
  after_transition(to: [:connected]) do |connection, current_transition|
48
48
  error = current_transition.metadata.reason
49
49
  if is_error_type?(error)
50
- connection.logger.warn "ConnectionManager: Connected with error - #{error.message}"
51
- connection.emit :error, error
50
+ connection.logger.warn { "ConnectionManager: Connected with error - #{error.message}" }
52
51
  end
53
52
  end
54
53
 
@@ -62,6 +61,11 @@ module Ably::Realtime
62
61
  connection.manager.respond_to_transport_disconnected_whilst_connected err
63
62
  end
64
63
 
64
+ after_transition(to: [:suspended]) do |connection, current_transition|
65
+ err = error_from_state_change(current_transition)
66
+ connection.manager.suspend_active_channels err
67
+ end
68
+
65
69
  after_transition(to: [:disconnected, :suspended]) do |connection|
66
70
  connection.manager.destroy_transport # never reuse a transport if the connection has failed
67
71
  end
@@ -71,6 +75,18 @@ module Ably::Realtime
71
75
  connection.manager.fail err
72
76
  end
73
77
 
78
+ after_transition(to: [:failed]) do |connection, current_transition|
79
+ err = error_from_state_change(current_transition)
80
+ connection.manager.fail_active_channels err
81
+ end
82
+
83
+ # RTN7C - If a connection enters the SUSPENDED, CLOSED or FAILED state...
84
+ # the client should consider the delivery of those messages as failed
85
+ after_transition(to: [:suspended, :closed, :failed]) do |connection, current_transition|
86
+ err = error_from_state_change(current_transition)
87
+ connection.manager.nack_messages_on_all_channels err
88
+ end
89
+
74
90
  after_transition(to: [:closing], from: [:initialized, :disconnected, :suspended]) do |connection|
75
91
  connection.manager.force_close_connection
76
92
  end
@@ -83,6 +99,10 @@ module Ably::Realtime
83
99
  connection.manager.destroy_transport
84
100
  end
85
101
 
102
+ after_transition(to: [:closed]) do |connection|
103
+ connection.manager.detach_active_channels
104
+ end
105
+
86
106
  # Transitions responsible for updating connection#error_reason
87
107
  before_transition(to: [:disconnected, :suspended, :failed]) do |connection, current_transition|
88
108
  err = error_from_state_change(current_transition)
@@ -91,7 +111,6 @@ module Ably::Realtime
91
111
 
92
112
  before_transition(to: [:connected, :closed]) do |connection, current_transition|
93
113
  err = error_from_state_change(current_transition)
94
-
95
114
  if err
96
115
  connection.set_failed_connection_error_reason err
97
116
  else
@@ -119,14 +119,14 @@ module Ably::Realtime
119
119
  when :msgpack
120
120
  driver.binary(object.to_msgpack.unpack('C*'))
121
121
  else
122
- client.logger.fatal "WebsocketTransport: Unsupported protocol '#{client.protocol}' for serialization, object cannot be serialized and sent to Ably over this WebSocket"
122
+ client.logger.fatal { "WebsocketTransport: Unsupported protocol '#{client.protocol}' for serialization, object cannot be serialized and sent to Ably over this WebSocket" }
123
123
  end
124
124
  end
125
125
 
126
126
  def setup_event_handlers
127
127
  __outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
128
128
  send_object protocol_message
129
- client.logger.debug "WebsocketTransport: Prot msg sent =>: #{protocol_message.action} #{protocol_message}"
129
+ client.logger.debug { "WebsocketTransport: Prot msg sent =>: #{protocol_message.action} #{protocol_message}" }
130
130
  end
131
131
  end
132
132
 
@@ -147,32 +147,41 @@ module Ably::Realtime
147
147
  @driver = WebSocket::Driver.client(self)
148
148
 
149
149
  driver.on("open") do
150
- logger.debug "WebsocketTransport: socket opened to #{url}, waiting for Connected protocol message"
150
+ logger.debug { "WebsocketTransport: socket opened to #{url}, waiting for Connected protocol message" }
151
151
  end
152
152
 
153
153
  driver.on("message") do |event|
154
154
  event_data = parse_event_data(event.data).freeze
155
155
  protocol_message = Ably::Models::ProtocolMessage.new(event_data, logger: logger)
156
156
  action_name = Ably::Models::ProtocolMessage::ACTION[event_data['action']] rescue event_data['action']
157
- logger.debug "WebsocketTransport: Prot msg recv <=: #{action_name} - #{event_data}"
157
+ logger.debug { "WebsocketTransport: Prot msg recv <=: #{action_name} - #{event_data}" }
158
158
 
159
159
  if protocol_message.invalid?
160
- error = Ably::Exceptions::ProtocolError.new("Invalid Protocol Message received: #{event_data}\nMessage has been discarded", 400, 80013)
161
- connection.emit :error, error
162
- logger.fatal "WebsocketTransport: #{error.message}"
160
+ error = Ably::Exceptions::ProtocolError.new("Invalid Protocol Message received: #{event_data}\nConnection moving to the failed state as the protocol is invalid and unsupported", 400, 80013)
161
+ logger.fatal { "WebsocketTransport: #{error.message}" }
162
+ failed_protocol_message = Ably::Models::ProtocolMessage.new(
163
+ action: Ably::Models::ProtocolMessage::ACTION.Error,
164
+ error: error.as_json,
165
+ logger: logger
166
+ )
167
+ __incoming_protocol_msgbus__.publish :protocol_message, failed_protocol_message
163
168
  else
164
169
  __incoming_protocol_msgbus__.publish :protocol_message, protocol_message
165
170
  end
166
171
  end
167
172
 
173
+ driver.on("ping") do
174
+ __incoming_protocol_msgbus__.publish :protocol_message, Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Heartbeat, source: :websocket)
175
+ end
176
+
168
177
  driver.on("error") do |error|
169
- logger.error "WebsocketTransport: Protocol Error on transports - #{error.message}"
178
+ logger.error { "WebsocketTransport: Protocol Error on transports - #{error.message}" }
170
179
  end
171
180
 
172
181
  @reason_closed = nil
173
182
  driver.on("closed") do |event|
174
183
  @reason_closed = "#{event.code}: #{event.reason}"
175
- logger.warn "WebsocketTransport: Driver reported transport as closed - #{reason_closed}"
184
+ logger.warn { "WebsocketTransport: Driver reported transport as closed - #{reason_closed}" }
176
185
  end
177
186
  end
178
187
 
@@ -192,7 +201,7 @@ module Ably::Realtime
192
201
  when :msgpack
193
202
  MessagePack.unpack(data.pack('C*'))
194
203
  else
195
- client.logger.fatal "WebsocketTransport: Unsupported Protocol Message format #{client.protocol}"
204
+ client.logger.fatal { "WebsocketTransport: Unsupported Protocol Message format #{client.protocol}" }
196
205
  data
197
206
  end
198
207
  end
@@ -13,8 +13,7 @@ module Ably::Realtime
13
13
  :entering,
14
14
  :entered,
15
15
  :leaving,
16
- :left,
17
- :failed
16
+ :left
18
17
  )
19
18
  include Ably::Modules::StateEmitter
20
19
  include Ably::Modules::UsesStateMachine
@@ -23,11 +22,6 @@ module Ably::Realtime
23
22
  # @return [Ably::Realtime::Channel]
24
23
  attr_reader :channel
25
24
 
26
- # A unique identifier for this channel client based on their connection, disambiguating situations
27
- # where a given client_id is present on multiple connections simultaneously.
28
- # @return [String]
29
- attr_reader :connection_id
30
-
31
25
  # The client_id for the member present on this channel
32
26
  # @return [String]
33
27
  attr_reader :client_id
@@ -72,13 +66,16 @@ module Ably::Realtime
72
66
 
73
67
  return deferrable_succeed(deferrable, &success_block) if state == STATE.Entered
74
68
 
75
- ensure_presence_publishable_on_connection
69
+ requirements_failed_deferrable = ensure_presence_publishable_on_connection_deferrable
70
+ return requirements_failed_deferrable if requirements_failed_deferrable
71
+
76
72
  ensure_channel_attached(deferrable) do
77
73
  if entering?
78
74
  once_or_if(STATE.Entered, else: proc { |args| deferrable_fail deferrable, *args }) do
79
75
  deferrable_succeed deferrable, &success_block
80
76
  end
81
77
  else
78
+ current_state = state
82
79
  change_state STATE.Entering
83
80
  send_protocol_message_and_transition_state_to(
84
81
  Ably::Models::PresenceMessage::ACTION.Enter,
@@ -86,7 +83,7 @@ module Ably::Realtime
86
83
  target_state: STATE.Entered,
87
84
  data: data,
88
85
  client_id: client_id,
89
- failed_state: STATE.Failed,
86
+ failed_state: current_state, # return to current state if enter fails
90
87
  &success_block
91
88
  )
92
89
  end
@@ -125,19 +122,21 @@ module Ably::Realtime
125
122
  deferrable = create_deferrable
126
123
 
127
124
  ensure_supported_payload data
128
- raise Ably::Exceptions::Standard.new('Unable to leave presence channel that is not entered', 400, 91002) unless able_to_leave?
129
125
 
130
126
  @data = data
131
127
 
132
128
  return deferrable_succeed(deferrable, &success_block) if state == STATE.Left
133
129
 
134
- ensure_presence_publishable_on_connection
130
+ requirements_failed_deferrable = ensure_presence_publishable_on_connection_deferrable
131
+ return requirements_failed_deferrable if requirements_failed_deferrable
132
+
135
133
  ensure_channel_attached(deferrable) do
136
134
  if leaving?
137
135
  once_or_if(STATE.Left, else: proc { |error|deferrable_fail deferrable, *args }) do
138
136
  deferrable_succeed deferrable, &success_block
139
137
  end
140
138
  else
139
+ current_state = state
141
140
  change_state STATE.Leaving
142
141
  send_protocol_message_and_transition_state_to(
143
142
  Ably::Models::PresenceMessage::ACTION.Leave,
@@ -145,7 +144,7 @@ module Ably::Realtime
145
144
  target_state: STATE.Left,
146
145
  data: data,
147
146
  client_id: client_id,
148
- failed_state: STATE.Failed,
147
+ failed_state: current_state, # return to current state if leave fails
149
148
  &success_block
150
149
  )
151
150
  end
@@ -183,7 +182,9 @@ module Ably::Realtime
183
182
 
184
183
  @data = data
185
184
 
186
- ensure_presence_publishable_on_connection
185
+ requirements_failed_deferrable = ensure_presence_publishable_on_connection_deferrable
186
+ return requirements_failed_deferrable if requirements_failed_deferrable
187
+
187
188
  ensure_channel_attached(deferrable) do
188
189
  send_protocol_message_and_transition_state_to(
189
190
  Ably::Models::PresenceMessage::ACTION.Update,
@@ -214,7 +215,7 @@ module Ably::Realtime
214
215
  send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Update, client_id, data, &success_block)
215
216
  end
216
217
 
217
- # Get the presence state for this Channel.
218
+ # Get the presence members for this Channel.
218
219
  #
219
220
  # @param (see Ably::Realtime::Presence::MembersMap#get)
220
221
  # @option options (see Ably::Realtime::Presence::MembersMap#get)
@@ -224,11 +225,25 @@ module Ably::Realtime
224
225
  def get(options = {}, &block)
225
226
  deferrable = create_deferrable
226
227
 
227
- ensure_channel_attached(deferrable) do
228
+ # #RTP11d Don't return PresenceMap when wait for sync is true
229
+ # if the map is stale
230
+ wait_for_sync = options.fetch(:wait_for_sync, true)
231
+ if wait_for_sync && channel.suspended?
232
+ EventMachine.next_tick do
233
+ deferrable.fail Ably::Exceptions::InvalidState.new(
234
+ 'Presence state is out of sync as channel is SUSPENDED. Presence#get on a SUSPENDED channel is only supported with option wait_for_sync: false',
235
+ nil,
236
+ 91005
237
+ )
238
+ end
239
+ return deferrable
240
+ end
241
+
242
+ ensure_channel_attached(deferrable, allow_suspended: true) do
228
243
  members.get(options).tap do |members_map_deferrable|
229
- members_map_deferrable.callback do |*args|
230
- safe_yield(block, *args) if block_given?
231
- deferrable.succeed(*args)
244
+ members_map_deferrable.callback do |members|
245
+ safe_yield(block, members) if block_given?
246
+ deferrable.succeed(members)
232
247
  end
233
248
  members_map_deferrable.errback do |*args|
234
249
  deferrable.fail(*args)
@@ -246,9 +261,8 @@ module Ably::Realtime
246
261
  # @return [void]
247
262
  #
248
263
  def subscribe(*actions, &callback)
249
- ensure_channel_attached do
250
- super
251
- end
264
+ implicit_attach
265
+ super
252
266
  end
253
267
 
254
268
  # Unsubscribe the matching block for presence events on the associated Channel.
@@ -279,7 +293,10 @@ module Ably::Realtime
279
293
  #
280
294
  def history(options = {}, &callback)
281
295
  if options.delete(:until_attach)
282
- raise ArgumentError, 'option :until_attach cannot be specified if the channel is not attached' unless channel.attached?
296
+ unless channel.attached?
297
+ error = Ably::Exceptions::InvalidRequest.new('option :until_attach is invalid as the channel is not attached')
298
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
299
+ end
283
300
  options[:from_serial] = channel.attached_serial
284
301
  end
285
302
 
@@ -297,13 +314,6 @@ module Ably::Realtime
297
314
  )
298
315
  end
299
316
 
300
- # Configure the connection ID for this presence channel.
301
- # Typically configured only once when a user first enters a presence channel.
302
- # @api private
303
- def set_connection_id(new_connection_id)
304
- @connection_id = new_connection_id
305
- end
306
-
307
317
  # Used by {Ably::Modules::StateEmitter} to debug action changes
308
318
  # @api private
309
319
  def logger
@@ -316,10 +326,6 @@ module Ably::Realtime
316
326
  end
317
327
 
318
328
  private
319
- def able_to_leave?
320
- entering? || entered?
321
- end
322
-
323
329
  # @return [Ably::Models::PresenceMessage] presence message is returned allowing callbacks to be added
324
330
  def send_presence_protocol_message(presence_action, client_id, data)
325
331
  presence_message = create_presence_message(presence_action, client_id, data)
@@ -346,34 +352,42 @@ module Ably::Realtime
346
352
  }
347
353
 
348
354
  Ably::Models::PresenceMessage.new(model, logger: logger).tap do |presence_message|
349
- presence_message.encode self.channel
355
+ presence_message.encode(client.encoders, channel.options) do |encode_error, error_message|
356
+ client.logger.error error_message
357
+ end
350
358
  end
351
359
  end
352
360
 
353
- def ensure_presence_publishable_on_connection
361
+ def ensure_presence_publishable_on_connection_deferrable
354
362
  if !connection.can_publish_messages?
355
- raise Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. Client is configured to disallow queueing of messages and connection is currently #{connection.state}")
363
+ error = Ably::Exceptions::MessageQueueingDisabled.new("Presence event cannot be published as they cannot be queued when the connection is #{connection.state}")
364
+ Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
356
365
  end
357
366
  end
358
367
 
359
- def ensure_channel_attached(deferrable = nil)
368
+ def ensure_channel_attached(deferrable = nil, options = {})
360
369
  if channel.attached?
361
370
  yield
371
+ elsif options[:allow_suspended] && channel.suspended?
372
+ yield
362
373
  else
363
- attach_channel_then { yield }
374
+ attach_channel_then(deferrable) { yield }
364
375
  end
365
376
  deferrable
366
377
  end
367
378
 
368
379
  def ensure_supported_client_id(check_client_id)
369
380
  unless check_client_id
370
- raise Ably::Exceptions::IncompatibleClientId.new('Unable to enter/update/leave presence channel without a client_id', 400, 40012)
381
+ raise Ably::Exceptions::IncompatibleClientId.new('Unable to enter/update/leave presence channel without a client_id')
371
382
  end
372
383
  if check_client_id == '*'
373
- raise Ably::Exceptions::IncompatibleClientId.new('Unable to enter/update/leave presence channel with the reserved wildcard client_id', 400, 40012)
384
+ raise Ably::Exceptions::IncompatibleClientId.new('Unable to enter/update/leave presence channel with the reserved wildcard client_id')
385
+ end
386
+ unless check_client_id.kind_of?(String)
387
+ raise Ably::Exceptions::IncompatibleClientId.new('Unable to enter/update/leave with a non String client_id value')
374
388
  end
375
389
  unless client.auth.can_assume_client_id?(check_client_id)
376
- raise Ably::Exceptions::IncompatibleClientId.new("Cannot enter with provided client_id '#{check_client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'", 400, 40012)
390
+ raise Ably::Exceptions::IncompatibleClientId.new("Cannot enter with provided client_id '#{check_client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'")
377
391
  end
378
392
  end
379
393
 
@@ -409,7 +423,8 @@ module Ably::Realtime
409
423
  end
410
424
 
411
425
  def send_presence_action_for_client(action, client_id, data, &success_block)
412
- ensure_presence_publishable_on_connection
426
+ requirements_failed_deferrable = ensure_presence_publishable_on_connection_deferrable
427
+ return requirements_failed_deferrable if requirements_failed_deferrable
413
428
 
414
429
  deferrable = create_deferrable
415
430
  ensure_channel_attached(deferrable) do
@@ -420,15 +435,25 @@ module Ably::Realtime
420
435
  end
421
436
  end
422
437
 
423
- def attach_channel_then
438
+ def attach_channel_then(deferrable)
424
439
  if channel.detached? || channel.failed?
425
- raise Ably::Exceptions::InvalidStateChange.new("Operation is not allowed when channel is in #{channel.state}", 400, 91001)
440
+ deferrable.fail Ably::Exceptions::InvalidState.new("Operation is not allowed when channel is in #{channel.state}", 400, 91001)
426
441
  else
427
- channel.unsafe_once(Channel::STATE.Attached) { yield }
442
+ channel.unsafe_once(:attached, :detached, :failed) do |channel_state_change|
443
+ if channel_state_change.current == :attached
444
+ yield
445
+ else
446
+ deferrable.fail Ably::Exceptions::InvalidState.new("Operation failed as channel transitioned to #{channel_state_change.current}", 400, 91001)
447
+ end
448
+ end
428
449
  channel.attach
429
450
  end
430
451
  end
431
452
 
453
+ def implicit_attach
454
+ channel.attach if channel.initialized?
455
+ end
456
+
432
457
  def client
433
458
  channel.client
434
459
  end
@@ -20,7 +20,9 @@ module Ably::Realtime
20
20
 
21
21
  STATE = ruby_enum('STATE',
22
22
  :initialized,
23
- :sync_starting,
23
+ :sync_starting, # Indicates the client is waiting for SYNC ProtocolMessages from Ably
24
+ :sync_none, # Indicates the ATTACHED ProtocolMessage had no presence flag and thus no members on the channel
25
+ :finalizing_sync,
24
26
  :in_sync,
25
27
  :failed
26
28
  )
@@ -29,10 +31,21 @@ module Ably::Realtime
29
31
  def initialize(presence)
30
32
  @presence = presence
31
33
 
32
- @state = STATE(:initialized)
33
- @members = Hash.new
34
+ @state = STATE(:initialized)
35
+
36
+ # Two sets of members maintained
37
+ # @members contains all members present on the channel
38
+ # @local_members contains only this connection's members for the purpose of re-entering the member if channel continuity is lost
39
+ reset_members
40
+ reset_local_members
41
+
34
42
  @absent_member_cleanup_queue = []
35
43
 
44
+ # Each SYNC session has a unique ID so that following SYNC
45
+ # any members present in the map without this session ID are
46
+ # not present according to Ably, see #RTP19
47
+ @sync_session_id = -1
48
+
36
49
  setup_event_handlers
37
50
  end
38
51
 
@@ -54,7 +67,16 @@ module Ably::Realtime
54
67
  # @api private
55
68
  def update_sync_serial(serial)
56
69
  @sync_serial = serial
57
- change_state :in_sync if sync_serial_cursor_at_end?
70
+ end
71
+
72
+ # When channel serial in ProtocolMessage SYNC is nil or
73
+ # an empty cursor appears after the ':' such as 'cf30e75054887:psl_7g:client:189'.
74
+ # That is an indication that there are no more SYNC messages.
75
+ #
76
+ # @api private
77
+ #
78
+ def sync_serial_cursor_at_end?
79
+ sync_serial.nil? || sync_serial.to_s.match(/^[\w-]+:?$/)
58
80
  end
59
81
 
60
82
  # Get the list of presence members
@@ -62,14 +84,14 @@ module Ably::Realtime
62
84
  # @param [Hash,String] options an options Hash to filter members
63
85
  # @option options [String] :client_id optional client_id filter for the member
64
86
  # @option options [String] :connection_id optional connection_id filter for the member
65
- # @option options [String] :wait_for_sync defaults to false, if true the get method waits for the initial presence sync following channel attachment to complete before returning the members present
87
+ # @option options [String] :wait_for_sync defaults to true, if true the get method waits for the initial presence sync following channel attachment to complete before returning the members present, else it immediately returns the members present currently
66
88
  #
67
89
  # @yield [Array<Ably::Models::PresenceMessage>] array of present members
68
90
  #
69
91
  # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
70
92
  #
71
93
  def get(options = {}, &block)
72
- wait_for_sync = options.fetch(:wait_for_sync, false)
94
+ wait_for_sync = options.fetch(:wait_for_sync, true)
73
95
  deferrable = Ably::Util::SafeDeferrable.new(logger)
74
96
 
75
97
  result_block = proc do
@@ -104,9 +126,9 @@ module Ably::Realtime
104
126
  channel.off(&failed_callback)
105
127
  end
106
128
 
107
- once(:in_sync, &in_sync_callback)
129
+ unsafe_once(:in_sync, &in_sync_callback)
130
+ unsafe_once(:failed, &failed_callback)
108
131
 
109
- once(:failed, &failed_callback)
110
132
  channel.unsafe_once(:detaching, :detached, :failed) do |error_reason|
111
133
  failed_callback.call error_reason
112
134
  end
@@ -130,7 +152,19 @@ module Ably::Realtime
130
152
  present_members.each(&block)
131
153
  end
132
154
 
155
+ # A copy of the local members present i.e. members entered from this connection
156
+ # and thus the responsibility of this library to re-enter on the channel automatically if the
157
+ # channel loses continuity
158
+ #
159
+ # @return [Array<PresenceMessage>]
160
+ # @api private
161
+ def local_members
162
+ @local_members
163
+ end
164
+
133
165
  private
166
+ attr_reader :sync_session_id
167
+
134
168
  def members
135
169
  @members
136
170
  end
@@ -147,6 +181,14 @@ module Ably::Realtime
147
181
  @absent_member_cleanup_queue
148
182
  end
149
183
 
184
+ def reset_members
185
+ @members = Hash.new
186
+ end
187
+
188
+ def reset_local_members
189
+ @local_members = Hash.new
190
+ end
191
+
150
192
  def channel
151
193
  presence.channel
152
194
  end
@@ -165,18 +207,84 @@ module Ably::Realtime
165
207
 
166
208
  def setup_event_handlers
167
209
  presence.__incoming_msgbus__.subscribe(:presence, :sync) do |presence_message|
168
- presence_message.decode channel
210
+ presence_message.decode(client.encoders, channel.options) do |encode_error, error_message|
211
+ client.logger.error error_message
212
+ end
169
213
  update_members_and_emit_events presence_message
170
214
  end
171
215
 
216
+ channel.unsafe_on(:failed, :detached) do
217
+ reset_members
218
+ reset_local_members
219
+ end
220
+
172
221
  resume_sync_proc = method(:resume_sync).to_proc
173
- connection.on_resume(&resume_sync_proc)
174
- once(:in_sync, :failed) do
175
- connection.off_resume(&resume_sync_proc)
222
+
223
+ unsafe_on(:sync_starting) do
224
+ @sync_session_id += 1
225
+
226
+ channel.unsafe_once(:attached) do
227
+ connection.on_resume(&resume_sync_proc)
228
+ end
229
+
230
+ unsafe_once(:in_sync, :failed) do
231
+ connection.off_resume(&resume_sync_proc)
232
+ end
233
+ end
234
+
235
+ unsafe_on(:sync_none) do
236
+ @sync_session_id += 1
237
+ # Immediately change to finalizing which will result in all members being cleaned up
238
+ change_state :finalizing_sync
176
239
  end
177
240
 
178
- once(:in_sync) do
241
+ unsafe_on(:finalizing_sync) do
179
242
  clean_up_absent_members
243
+ clean_up_members_not_present_in_sync
244
+ change_state :in_sync
245
+ end
246
+
247
+ unsafe_on(:in_sync) do
248
+ update_local_member_state
249
+ end
250
+ end
251
+
252
+ # Listen for events that change the PresenceMap state and thus
253
+ # need to be replicated to the local member set
254
+ def update_local_member_state
255
+ new_local_members = members.select do |member_key, member|
256
+ member.fetch(:message).connection_id == connection.id
257
+ end.each_with_object({}) do |(member_key, member), hash_object|
258
+ hash_object[member_key] = member.fetch(:message)
259
+ end
260
+
261
+ @local_members.reject do |member_key, message|
262
+ new_local_members.keys.include?(member_key)
263
+ end.each do |member_key, message|
264
+ re_enter_local_member_missing_from_presence_map message
265
+ end
266
+
267
+ @local_members = new_local_members
268
+ end
269
+
270
+ def re_enter_local_member_missing_from_presence_map(presence_message)
271
+ local_client_id = presence_message.client_id || client.auth.client_id
272
+ logger.debug { "#{self.class.name}: Manually re-entering local presence member, client ID: #{local_client_id} with data: #{presence_message.data}" }
273
+ presence.enter_client(local_client_id, presence_message.data).tap do |deferrable|
274
+ deferrable.errback do |error|
275
+ presence_message_client_id = presence_message.client_id || client.auth.client_id
276
+ re_enter_error = Ably::Models::ErrorInfo.new(
277
+ message: "unable to automatically re-enter presence channel for client_id '#{presence_message_client_id}'. Source error code #{error.code} and message '#{error.message}'",
278
+ code: 91004
279
+ )
280
+ channel.emit :update, Ably::Models::ChannelStateChange.new(
281
+ current: channel.state,
282
+ previous: channel.state,
283
+ event: Ably::Realtime::Channel::EVENT(:update),
284
+ reason: re_enter_error,
285
+ resumed: true
286
+ )
287
+ end
180
288
  end
181
289
  end
182
290
 
@@ -186,21 +294,15 @@ module Ably::Realtime
186
294
  action: Ably::Models::ProtocolMessage::ACTION.Sync.to_i,
187
295
  channel: channel.name,
188
296
  channel_serial: sync_serial
189
- )
190
- end
191
-
192
- # When channel serial in ProtocolMessage SYNC is nil or
193
- # an empty cursor appears after the ':' such as 'cf30e75054887:psl_7g:client:189'.
194
- # That is an indication that there are no more SYNC messages.
195
- def sync_serial_cursor_at_end?
196
- sync_serial.nil? || sync_serial.to_s.match(/^[\w-]+:?$/)
297
+ ) if channel.attached?
197
298
  end
198
299
 
199
300
  def update_members_and_emit_events(presence_message)
200
301
  return unless ensure_presence_message_is_valid(presence_message)
201
302
 
202
303
  unless should_update_member?(presence_message)
203
- logger.debug "#{self.class.name}: Skipped presence member #{presence_message.action} on channel #{presence.channel.name}.\n#{presence_message.to_safe_json}"
304
+ logger.debug { "#{self.class.name}: Skipped presence member #{presence_message.action} on channel #{presence.channel.name}.\n#{presence_message.to_json}" }
305
+ touch_presence_member presence_message
204
306
  return
205
307
  end
206
308
 
@@ -218,45 +320,94 @@ module Ably::Realtime
218
320
  return true if presence_message.connection_id
219
321
 
220
322
  error = Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing connectionId", 400, 80013)
221
- logger.error "PresenceMap: On channel '#{channel.name}' error: #{error}"
222
- channel.emit :error, error
323
+ logger.error { "PresenceMap: On channel '#{channel.name}' error: #{error}" }
223
324
  end
224
325
 
225
326
  # If the message received is older than the last known event for presence
226
- # then skip. This can occur during a SYNC operation. For example:
327
+ # then skip (return false). This can occur during a SYNC operation. For example:
227
328
  # - SYNC starts
228
329
  # - LEAVE event received for clientId 5
229
330
  # - SYNC present even received for clientId 5 with a timestamp before LEAVE event because the LEAVE occured before the SYNC operation completed
230
331
  #
231
- # @return [Boolean]
332
+ # @return [Boolean] true when +new_message+ is newer than the existing member in the PresenceMap
232
333
  #
233
- def should_update_member?(presence_message)
234
- if members[presence_message.member_key]
235
- members[presence_message.member_key].fetch(:message).timestamp < presence_message.timestamp
334
+ def should_update_member?(new_message)
335
+ if members[new_message.member_key]
336
+ existing_message = members[new_message.member_key].fetch(:message)
337
+
338
+ # If both are messages published by clients (not fabricated), use the ID to determine newness, see #RTP2b2
339
+ if new_message.id.start_with?(new_message.connection_id) && existing_message.id.start_with?(existing_message.connection_id)
340
+ new_message_parts = new_message.id.match(/(\d+):(\d+)$/)
341
+ existing_message_parts = existing_message.id.match(/(\d+):(\d+)$/)
342
+
343
+ if !new_message_parts || !existing_message_parts
344
+ logger.fatal { "#{self.class.name}: Message IDs for new message #{new_message.id} or old message #{existing_message.id} are invalid. \nNew message: #{new_message.to_json}" }
345
+ return existing_message.timestamp < new_message.timestamp
346
+ end
347
+
348
+ # ID is in the format "connid:msgSerial:index" such as "aaaaaa:0:0"
349
+ # if msgSerial is greater then the new_message should update the member
350
+ # if msgSerial is equal and index is greater, then update the member
351
+ if new_message_parts[1].to_i > existing_message_parts[1].to_i # msgSerial
352
+ true
353
+ elsif new_message_parts[1].to_i == existing_message_parts[1].to_i # msgSerial equal
354
+ new_message_parts[2].to_i > existing_message_parts[2].to_i # compare index
355
+ else
356
+ false
357
+ end
358
+ else
359
+ # This message is fabricated or could not be validated so rely on timestamps, see #RTP2b1
360
+ new_message.timestamp > existing_message.timestamp
361
+ end
236
362
  else
237
363
  true
238
364
  end
239
365
  end
240
366
 
241
367
  def add_presence_member(presence_message)
242
- logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' for event '#{presence_message.action}' #{members.has_key?(presence_message.member_key) ? 'updated' : 'added'}.\n#{presence_message.to_safe_json}"
243
- members[presence_message.member_key] = { present: true, message: presence_message }
368
+ logger.debug { "#{self.class.name}: Member '#{presence_message.member_key}' for event '#{presence_message.action}' #{members.has_key?(presence_message.member_key) ? 'updated' : 'added'}.\n#{presence_message.to_json}" }
369
+ # Mutate the PresenceMessage so that the action is :present, see #RTP2d
370
+ present_presence_message = presence_message.shallow_clone(action: Ably::Models::PresenceMessage::ACTION.Present)
371
+ member_set_upsert present_presence_message, true
244
372
  presence.emit_message presence_message.action, presence_message
245
373
  end
246
374
 
247
375
  def remove_presence_member(presence_message)
248
- logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_safe_json}"
376
+ logger.debug { "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_json}" }
249
377
 
250
378
  if in_sync?
251
- members.delete presence_message.member_key
379
+ member_set_delete presence_message
252
380
  else
253
- members[presence_message.member_key] = { present: false, message: presence_message }
254
- absent_member_cleanup_queue << presence_message.member_key
381
+ member_set_upsert presence_message, false
382
+ absent_member_cleanup_queue << presence_message
255
383
  end
256
384
 
257
385
  presence.emit_message presence_message.action, presence_message
258
386
  end
259
387
 
388
+ # No update is necessary for this member as older / no change during update
389
+ # however we need to update the sync_session_id so that this member is not removed following SYNC
390
+ def touch_presence_member(presence_message)
391
+ members.fetch(presence_message.member_key)[:sync_session_id] = sync_session_id
392
+ end
393
+
394
+ def member_set_upsert(presence_message, present)
395
+ members[presence_message.member_key] = { present: present, message: presence_message, sync_session_id: sync_session_id }
396
+ if presence_message.connection_id == connection.id
397
+ local_members[presence_message.member_key] = presence_message
398
+ logger.debug { "#{self.class.name}: Local member '#{presence_message.member_key}' added" }
399
+ end
400
+ end
401
+
402
+ def member_set_delete(presence_message)
403
+ members.delete presence_message.member_key
404
+ if in_sync?
405
+ # If not in SYNC, then local members missing may need to be re-entered
406
+ # Let #update_local_member_state handle missing members
407
+ local_members.delete presence_message.member_key
408
+ end
409
+ end
410
+
260
411
  def present_members
261
412
  members.select do |key, presence|
262
413
  presence.fetch(:present)
@@ -274,7 +425,21 @@ module Ably::Realtime
274
425
  end
275
426
 
276
427
  def clean_up_absent_members
277
- members.delete absent_member_cleanup_queue.shift
428
+ while member_to_remove = absent_member_cleanup_queue.shift
429
+ logger.debug { "#{self.class.name}: Cleaning up absent member '#{member_to_remove.member_key}' after SYNC.\n#{member_to_remove.to_json}" }
430
+ member_set_delete member_to_remove
431
+ end
432
+ end
433
+
434
+ def clean_up_members_not_present_in_sync
435
+ members.select do |member_key, member|
436
+ member.fetch(:sync_session_id) != sync_session_id
437
+ end.each do |member_key, member|
438
+ presence_message = member.fetch(:message).shallow_clone(action: Ably::Models::PresenceMessage::ACTION.Leave, id: nil)
439
+ logger.debug { "#{self.class.name}: Fabricating a LEAVE event for member '#{presence_message.member_key}' was not present in recently completed SYNC session ID '#{sync_session_id}'.\n#{presence_message.to_json}" }
440
+ member_set_delete member.fetch(:message)
441
+ presence.emit_message Ably::Models::PresenceMessage::ACTION.Leave, presence_message
442
+ end
278
443
  end
279
444
  end
280
445
  end