ably-rest 0.9.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/ably-rest.gemspec +2 -1
  3. data/lib/submodules/ably-ruby/.travis.yml +6 -4
  4. data/lib/submodules/ably-ruby/CHANGELOG.md +52 -61
  5. data/lib/submodules/ably-ruby/README.md +10 -0
  6. data/lib/submodules/ably-ruby/SPEC.md +1473 -852
  7. data/lib/submodules/ably-ruby/ably.gemspec +2 -1
  8. data/lib/submodules/ably-ruby/lib/ably/auth.rb +57 -25
  9. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +34 -8
  10. data/lib/submodules/ably-ruby/lib/ably/logger.rb +10 -1
  11. data/lib/submodules/ably-ruby/lib/ably/models/auth_details.rb +42 -0
  12. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +18 -4
  13. data/lib/submodules/ably-ruby/lib/ably/models/connection_details.rb +6 -3
  14. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +4 -3
  15. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +1 -1
  16. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +12 -1
  17. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +101 -97
  18. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +13 -1
  19. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +20 -3
  20. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +7 -3
  21. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +17 -7
  22. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +29 -14
  23. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +7 -4
  24. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -4
  25. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +7 -3
  26. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +2 -0
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +79 -31
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +62 -26
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +154 -65
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +16 -3
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +108 -49
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +165 -59
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -10
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +67 -45
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +198 -36
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +30 -6
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
  42. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +3 -3
  43. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +21 -8
  44. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +1 -3
  45. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +2 -2
  46. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  47. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +1 -1
  48. data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +26 -0
  49. data/lib/submodules/ably-ruby/lib/ably/version.rb +2 -2
  50. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +416 -99
  51. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +5 -3
  52. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +1011 -160
  53. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +2 -2
  54. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
  55. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +436 -97
  56. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +52 -23
  57. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +5 -3
  58. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1160 -105
  59. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +151 -22
  60. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +1 -1
  61. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +88 -27
  62. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +42 -15
  63. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +4 -4
  64. data/lib/submodules/ably-ruby/spec/rspec_config.rb +2 -1
  65. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +2 -2
  66. data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +6 -2
  67. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +20 -4
  68. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +32 -1
  69. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +4 -11
  70. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +28 -2
  71. data/lib/submodules/ably-ruby/spec/unit/models/auth_details_spec.rb +49 -0
  72. data/lib/submodules/ably-ruby/spec/unit/models/channel_state_change_spec.rb +23 -3
  73. data/lib/submodules/ably-ruby/spec/unit/models/connection_details_spec.rb +12 -1
  74. data/lib/submodules/ably-ruby/spec/unit/models/connection_state_change_spec.rb +15 -4
  75. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +34 -2
  76. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +73 -2
  77. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +64 -6
  78. data/lib/submodules/ably-ruby/spec/unit/models/token_details_spec.rb +1 -1
  79. data/lib/submodules/ably-ruby/spec/unit/models/token_request_spec.rb +1 -1
  80. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +2 -1
  81. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +69 -0
  82. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +149 -22
  83. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +9 -3
  84. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +1 -1
  85. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +8 -5
  86. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
  87. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +4 -3
  88. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +1 -1
  89. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +3 -3
  90. metadata +7 -5
@@ -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)
@@ -348,35 +354,40 @@ module Ably::Realtime
348
354
  Ably::Models::PresenceMessage.new(model, logger: logger).tap do |presence_message|
349
355
  presence_message.encode(client.encoders, channel.options) do |encode_error, error_message|
350
356
  client.logger.error error_message
351
- emit :error, encode_error
352
357
  end
353
358
  end
354
359
  end
355
360
 
356
- def ensure_presence_publishable_on_connection
361
+ def ensure_presence_publishable_on_connection_deferrable
357
362
  if !connection.can_publish_messages?
358
- 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)
359
365
  end
360
366
  end
361
367
 
362
- def ensure_channel_attached(deferrable = nil)
368
+ def ensure_channel_attached(deferrable = nil, options = {})
363
369
  if channel.attached?
364
370
  yield
371
+ elsif options[:allow_suspended] && channel.suspended?
372
+ yield
365
373
  else
366
- attach_channel_then { yield }
374
+ attach_channel_then(deferrable) { yield }
367
375
  end
368
376
  deferrable
369
377
  end
370
378
 
371
379
  def ensure_supported_client_id(check_client_id)
372
380
  unless check_client_id
373
- 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')
374
382
  end
375
383
  if check_client_id == '*'
376
- 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')
377
388
  end
378
389
  unless client.auth.can_assume_client_id?(check_client_id)
379
- 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}'")
380
391
  end
381
392
  end
382
393
 
@@ -412,7 +423,8 @@ module Ably::Realtime
412
423
  end
413
424
 
414
425
  def send_presence_action_for_client(action, client_id, data, &success_block)
415
- ensure_presence_publishable_on_connection
426
+ requirements_failed_deferrable = ensure_presence_publishable_on_connection_deferrable
427
+ return requirements_failed_deferrable if requirements_failed_deferrable
416
428
 
417
429
  deferrable = create_deferrable
418
430
  ensure_channel_attached(deferrable) do
@@ -423,15 +435,25 @@ module Ably::Realtime
423
435
  end
424
436
  end
425
437
 
426
- def attach_channel_then
438
+ def attach_channel_then(deferrable)
427
439
  if channel.detached? || channel.failed?
428
- 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)
429
441
  else
430
- 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
431
449
  channel.attach
432
450
  end
433
451
  end
434
452
 
453
+ def implicit_attach
454
+ channel.attach if channel.initialized?
455
+ end
456
+
435
457
  def client
436
458
  channel.client
437
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
@@ -167,19 +209,82 @@ module Ably::Realtime
167
209
  presence.__incoming_msgbus__.subscribe(:presence, :sync) do |presence_message|
168
210
  presence_message.decode(client.encoders, channel.options) do |encode_error, error_message|
169
211
  client.logger.error error_message
170
- channel.emit :error, encode_error
171
212
  end
172
213
  update_members_and_emit_events presence_message
173
214
  end
174
215
 
216
+ channel.unsafe_on(:failed, :detached) do
217
+ reset_members
218
+ reset_local_members
219
+ end
220
+
175
221
  resume_sync_proc = method(:resume_sync).to_proc
176
- connection.on_resume(&resume_sync_proc)
177
- once(:in_sync, :failed) do
178
- 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
179
233
  end
180
234
 
181
- once(:in_sync) do
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
239
+ end
240
+
241
+ unsafe_on(:finalizing_sync) do
182
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
183
288
  end
184
289
  end
185
290
 
@@ -189,21 +294,15 @@ module Ably::Realtime
189
294
  action: Ably::Models::ProtocolMessage::ACTION.Sync.to_i,
190
295
  channel: channel.name,
191
296
  channel_serial: sync_serial
192
- )
193
- end
194
-
195
- # When channel serial in ProtocolMessage SYNC is nil or
196
- # an empty cursor appears after the ':' such as 'cf30e75054887:psl_7g:client:189'.
197
- # That is an indication that there are no more SYNC messages.
198
- def sync_serial_cursor_at_end?
199
- sync_serial.nil? || sync_serial.to_s.match(/^[\w-]+:?$/)
297
+ ) if channel.attached?
200
298
  end
201
299
 
202
300
  def update_members_and_emit_events(presence_message)
203
301
  return unless ensure_presence_message_is_valid(presence_message)
204
302
 
205
303
  unless should_update_member?(presence_message)
206
- logger.debug "#{self.class.name}: Skipped presence member #{presence_message.action} on channel #{presence.channel.name}.\n#{presence_message.to_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
207
306
  return
208
307
  end
209
308
 
@@ -221,45 +320,94 @@ module Ably::Realtime
221
320
  return true if presence_message.connection_id
222
321
 
223
322
  error = Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing connectionId", 400, 80013)
224
- logger.error "PresenceMap: On channel '#{channel.name}' error: #{error}"
225
- channel.emit :error, error
323
+ logger.error { "PresenceMap: On channel '#{channel.name}' error: #{error}" }
226
324
  end
227
325
 
228
326
  # If the message received is older than the last known event for presence
229
- # 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:
230
328
  # - SYNC starts
231
329
  # - LEAVE event received for clientId 5
232
330
  # - SYNC present even received for clientId 5 with a timestamp before LEAVE event because the LEAVE occured before the SYNC operation completed
233
331
  #
234
- # @return [Boolean]
332
+ # @return [Boolean] true when +new_message+ is newer than the existing member in the PresenceMap
235
333
  #
236
- def should_update_member?(presence_message)
237
- if members[presence_message.member_key]
238
- 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
239
362
  else
240
363
  true
241
364
  end
242
365
  end
243
366
 
244
367
  def add_presence_member(presence_message)
245
- 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}"
246
- 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
247
372
  presence.emit_message presence_message.action, presence_message
248
373
  end
249
374
 
250
375
  def remove_presence_member(presence_message)
251
- logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_json}"
376
+ logger.debug { "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_json}" }
252
377
 
253
378
  if in_sync?
254
- members.delete presence_message.member_key
379
+ member_set_delete presence_message
255
380
  else
256
- members[presence_message.member_key] = { present: false, message: presence_message }
257
- absent_member_cleanup_queue << presence_message.member_key
381
+ member_set_upsert presence_message, false
382
+ absent_member_cleanup_queue << presence_message
258
383
  end
259
384
 
260
385
  presence.emit_message presence_message.action, presence_message
261
386
  end
262
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
+
263
411
  def present_members
264
412
  members.select do |key, presence|
265
413
  presence.fetch(:present)
@@ -277,7 +425,21 @@ module Ably::Realtime
277
425
  end
278
426
 
279
427
  def clean_up_absent_members
280
- 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
281
443
  end
282
444
  end
283
445
  end