ably 1.2.5 → 1.2.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +1 -1
  3. data/CHANGELOG.md +17 -0
  4. data/README.md +24 -7
  5. data/SPEC.md +1722 -853
  6. data/ably.gemspec +1 -1
  7. data/lib/ably/auth.rb +19 -11
  8. data/lib/ably/models/protocol_message.rb +5 -26
  9. data/lib/ably/modules/safe_deferrable.rb +2 -2
  10. data/lib/ably/modules/state_emitter.rb +1 -1
  11. data/lib/ably/realtime/auth.rb +4 -0
  12. data/lib/ably/realtime/channel/channel_manager.rb +51 -48
  13. data/lib/ably/realtime/channel/channel_properties.rb +9 -0
  14. data/lib/ably/realtime/channel/channel_state_machine.rb +2 -0
  15. data/lib/ably/realtime/channel.rb +4 -3
  16. data/lib/ably/realtime/channels.rb +20 -0
  17. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -13
  18. data/lib/ably/realtime/client.rb +14 -6
  19. data/lib/ably/realtime/connection/connection_manager.rb +21 -22
  20. data/lib/ably/realtime/connection.rb +77 -109
  21. data/lib/ably/realtime/presence/members_map.rb +41 -92
  22. data/lib/ably/realtime/presence/presence_manager.rb +12 -17
  23. data/lib/ably/realtime/presence.rb +15 -6
  24. data/lib/ably/realtime/push.rb +0 -27
  25. data/lib/ably/realtime/recovery_key_context.rb +36 -0
  26. data/lib/ably/rest/client.rb +4 -6
  27. data/lib/ably/rest/push/admin.rb +1 -1
  28. data/lib/ably/rest/push.rb +0 -19
  29. data/lib/ably/util/ably_extensions.rb +29 -0
  30. data/lib/ably/util/crypto.rb +2 -2
  31. data/lib/ably/util/safe_deferrable.rb +1 -1
  32. data/lib/ably/version.rb +5 -7
  33. data/spec/acceptance/realtime/channel_history_spec.rb +8 -12
  34. data/spec/acceptance/realtime/channel_spec.rb +474 -300
  35. data/spec/acceptance/realtime/client_spec.rb +1 -1
  36. data/spec/acceptance/realtime/connection_failures_spec.rb +8 -25
  37. data/spec/acceptance/realtime/connection_spec.rb +28 -120
  38. data/spec/acceptance/realtime/message_spec.rb +23 -52
  39. data/spec/acceptance/realtime/presence_spec.rb +123 -92
  40. data/spec/acceptance/rest/channel_spec.rb +2 -2
  41. data/spec/acceptance/rest/client_spec.rb +9 -2
  42. data/spec/acceptance/rest/message_spec.rb +8 -11
  43. data/spec/acceptance/rest/push_admin_spec.rb +20 -15
  44. data/spec/shared/client_initializer_behaviour.rb +1 -1
  45. data/spec/support/markdown_spec_formatter.rb +1 -1
  46. data/spec/unit/models/protocol_message_spec.rb +0 -78
  47. data/spec/unit/models/token_details_spec.rb +4 -2
  48. data/spec/unit/realtime/channels_spec.rb +1 -1
  49. data/spec/unit/realtime/connection_spec.rb +0 -30
  50. data/spec/unit/realtime/recovery_key_context_spec.rb +36 -0
  51. data/spec/unit/util/crypto_spec.rb +15 -15
  52. metadata +9 -9
  53. data/spec/acceptance/realtime/push_spec.rb +0 -27
  54. data/spec/acceptance/rest/push_spec.rb +0 -25
@@ -9,6 +9,8 @@ module Ably
9
9
  include Ably::Modules::Conversions
10
10
  include Ably::Modules::SafeYield
11
11
  extend Ably::Modules::Enum
12
+ using Ably::Util::AblyExtensions
13
+
12
14
 
13
15
  # The current {Ably::Realtime::Connection::STATE} of the connection.
14
16
  # Describes the realtime [Connection]{@link Connection} object states.
@@ -23,26 +25,26 @@ module Ably
23
25
  # or no host is available. The disconnected state is entered if an established connection is dropped,
24
26
  # or if a connection attempt was unsuccessful. In the disconnected state the library will periodically
25
27
  # attempt to open a new connection (approximately every 15 seconds), anticipating that the connection
26
- # will be re-established soon and thus connection and channel continuity will be possible.
28
+ # will be re-established soon and thus connection and channel continuity will be possible.
27
29
  # In this state, developers can continue to publish messages as they are automatically placed
28
- # in a local queue, to be sent as soon as a connection is reestablished. Messages published by
30
+ # in a local queue, to be sent as soon as a connection is reestablished. Messages published by
29
31
  # other clients while this client is disconnected will be delivered to it upon reconnection,
30
- # so long as the connection was resumed within 2 minutes. After 2 minutes have elapsed, recovery
32
+ # so long as the connection was resumed within 2 minutes. After 2 minutes have elapsed, recovery
31
33
  # is no longer possible and the connection will move to the SUSPENDED state.
32
- # SUSPENDED A long term failure condition. No current connection exists because there is no network connectivity
33
- # or no host is available. The suspended state is entered after a failed connection attempt if
34
- # there has then been no connection for a period of two minutes. In the suspended state, the library
35
- # will periodically attempt to open a new connection every 30 seconds. Developers are unable to
34
+ # SUSPENDED A long term failure condition. No current connection exists because there is no network connectivity
35
+ # or no host is available. The suspended state is entered after a failed connection attempt if
36
+ # there has then been no connection for a period of two minutes. In the suspended state, the library
37
+ # will periodically attempt to open a new connection every 30 seconds. Developers are unable to
36
38
  # publish messages in this state. A new connection attempt can also be triggered by an explicit
37
- # call to {Ably::Realtime::Connection#connect}. Once the connection has been re-established,
38
- # channels will be automatically re-attached. The client has been disconnected for too long for them
39
- # to resume from where they left off, so if it wants to catch up on messages published by other clients
39
+ # call to {Ably::Realtime::Connection#connect}. Once the connection has been re-established,
40
+ # channels will be automatically re-attached. The client has been disconnected for too long for them
41
+ # to resume from where they left off, so if it wants to catch up on messages published by other clients
40
42
  # while it was disconnected, it needs to use the History API.
41
- # CLOSING An explicit request by the developer to close the connection has been sent to the Ably service.
42
- # If a reply is not received from Ably within a short period of time, the connection is forcibly
43
+ # CLOSING An explicit request by the developer to close the connection has been sent to the Ably service.
44
+ # If a reply is not received from Ably within a short period of time, the connection is forcibly
43
45
  # terminated and the connection state becomes CLOSED.
44
- # CLOSED The connection has been explicitly closed by the client. In the closed state, no reconnection attempts
45
- # are made automatically by the library, and clients may not publish messages. No connection state is
46
+ # CLOSED The connection has been explicitly closed by the client. In the closed state, no reconnection attempts
47
+ # are made automatically by the library, and clients may not publish messages. No connection state is
46
48
  # preserved by the service or by the library. A new connection attempt can be triggered by an explicit
47
49
  # call to {Ably::Realtime::Connection#connect}, which results in a new connection.
48
50
  # FAILED This state is entered if the client library encounters a failure condition that it cannot recover from.
@@ -77,9 +79,6 @@ module Ably
77
79
  include Ably::Modules::UsesStateMachine
78
80
  ensure_state_machine_emits 'Ably::Models::ConnectionStateChange'
79
81
 
80
- # Expected format for a connection recover key
81
- RECOVER_REGEX = /^(?<recover>[^:]+):(?<connection_serial>[^:]+):(?<msg_serial>\-?\d+)$/
82
-
83
82
  # Defaults for automatic connection recovery and timeouts
84
83
  DEFAULTS = {
85
84
  channel_retry_timeout: 15, # when a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED
@@ -113,16 +112,6 @@ module Ably
113
112
  #
114
113
  attr_reader :key
115
114
 
116
- # The serial number of the last message to be received on this connection, used automatically by the library when
117
- # recovering or resuming a connection. When recovering a connection explicitly, the recoveryKey is used in
118
- # the recover client options as it contains both the key and the last message serial.
119
- #
120
- # @spec RTN10
121
- #
122
- # @return [Integer]
123
- #
124
- attr_reader :serial
125
-
126
115
  # An {Ably::Models::ErrorInfo} object describing the last error received if a connection failure occurs.
127
116
  #
128
117
  # @spec RTN14a
@@ -177,17 +166,6 @@ module Ably
177
166
  end if options.kind_of?(Hash)
178
167
  @defaults.freeze
179
168
 
180
- # If a recover client options is provided, then we need to ensure that the msgSerial matches the
181
- # recover serial immediately at client library instantiation. This is done immediately so that any queued
182
- # publishes use the correct serial number for these queued messages as well.
183
- # There is no harm if the msgSerial is higher than expected if the recover fails.
184
- recovery_msg_serial = connection_recover_parts && connection_recover_parts[:msg_serial].to_i
185
- if recovery_msg_serial
186
- @client_msg_serial = recovery_msg_serial
187
- else
188
- reset_client_msg_serial
189
- end
190
-
191
169
  Client::IncomingMessageDispatcher.new client, self
192
170
  Client::OutgoingMessageDispatcher.new client, self
193
171
 
@@ -196,6 +174,8 @@ module Ably
196
174
  @manager = ConnectionManager.new(self)
197
175
 
198
176
  @current_host = client.endpoint.host
177
+
178
+ reset_client_msg_serial
199
179
  end
200
180
 
201
181
  # Causes the connection to close, entering the {Ably::Realtime::Connection::STATE} CLOSING state.
@@ -347,33 +327,40 @@ module Ably
347
327
  end
348
328
  end
349
329
 
350
- # The recovery key string can be used by another client to recover this connection's state in the recover client options property. See connection state recover options for more information.
330
+ # The recovery key string can be used by another client to recover this connection's state in the
331
+ # recover client options property. See connection state recover options for more information.
351
332
  #
352
333
  # @spec RTN16b, RTN16c
353
334
  #
354
- # @return [String]
335
+ # @deprecated Use {#create_recovery_key} instead
355
336
  #
356
337
  def recovery_key
357
- "#{key}:#{serial}:#{client_msg_serial}" if connection_resumable?
338
+ logger.warn "[DEPRECATION] recovery_key is deprecated, use create_recovery_key method instead"
339
+ create_recovery_key
340
+ end
341
+
342
+ # The recovery key string can be used by another client to recover this connection's state in the recover client
343
+ # options property. See connection state recover options for more information.
344
+ #
345
+ # @spec RTN16g, RTN16c
346
+ #
347
+ # @return [String] a json string which incorporates the @connectionKey@, the current @msgSerial@ and collection
348
+ # of pairs of channel @name@ and current @channelSerial@ for every currently attached channel
349
+ def create_recovery_key
350
+ if key.nil_or_empty? || state == :closing || state == :closed || state == :failed || state == :suspended
351
+ return nil #RTN16g2
352
+ end
353
+ RecoveryKeyContext.new(key, client_msg_serial, client.channels.get_channel_serials).to_json
358
354
  end
359
355
 
360
356
  # Following a new connection being made, the connection ID, connection key
361
- # and connection serial need to match the details provided by the server.
357
+ # need to match the details provided by the server.
362
358
  #
363
359
  # @return [void]
364
360
  # @api private
365
- def configure_new(connection_id, connection_key, connection_serial)
361
+ def configure_new(connection_id, connection_key)
366
362
  @id = connection_id
367
363
  @key = connection_key
368
-
369
- update_connection_serial connection_serial
370
- end
371
-
372
- # Store last received connection serial so that the connection can be resumed from the last known point-in-time
373
- # @return [void]
374
- # @api private
375
- def update_connection_serial(connection_serial)
376
- @serial = connection_serial
377
364
  end
378
365
 
379
366
  # Disable automatic resume of a connection
@@ -381,7 +368,7 @@ module Ably
381
368
  # @api private
382
369
  def reset_resume_info
383
370
  @key = nil
384
- @serial = nil
371
+ @id = nil
385
372
  end
386
373
 
387
374
  # @!attribute [r] __outgoing_protocol_msgbus__
@@ -444,17 +431,28 @@ module Ably
444
431
  # @api private
445
432
  def send_protocol_message(protocol_message)
446
433
  add_message_serial_if_ack_required_to(protocol_message) do
447
- Ably::Models::ProtocolMessage.new(protocol_message, logger: logger).tap do |message|
448
- add_message_to_outgoing_queue message
449
- notify_message_dispatcher_of_new_message message
450
- logger.debug { "Connection: Prot msg queued =>: #{message.action} #{message}" }
451
- end
434
+ message = Ably::Models::ProtocolMessage.new(protocol_message, logger: logger)
435
+ add_message_to_outgoing_queue(message)
436
+ notify_message_dispatcher_of_new_message message
452
437
  end
453
438
  end
454
439
 
440
+ def send_protocol_message_immediately(protocol_message)
441
+ message = Ably::Models::ProtocolMessage.new(protocol_message, logger: logger)
442
+ add_message_to_outgoing_queue(message, true)
443
+ notify_message_dispatcher_of_new_message message
444
+ end
445
+
455
446
  # @api private
456
- def add_message_to_outgoing_queue(protocol_message)
457
- __outgoing_message_queue__ << protocol_message
447
+ def add_message_to_outgoing_queue(protocol_message, send_immediately = false)
448
+ if send_immediately
449
+ # Adding msg at the top of the queue to get processed immediately while connection is CONNECTED
450
+ __outgoing_message_queue__.prepend(protocol_message)
451
+ logger.debug { "Connection: protocol msg pushed at the top =>: #{protocol_message.action} #{protocol_message}" }
452
+ else
453
+ __outgoing_message_queue__ << protocol_message
454
+ logger.debug { "Connection: protocol msg queued =>: #{protocol_message.action} #{protocol_message}" }
455
+ end
458
456
  end
459
457
 
460
458
  # @api private
@@ -472,7 +470,7 @@ module Ably
472
470
  url_params = auth_params.merge(
473
471
  'format' => client.protocol,
474
472
  'echo' => client.echo_messages,
475
- 'v' => Ably::PROTOCOL_VERSION,
473
+ 'v' => Ably::PROTOCOL_VERSION, # RSC7a
476
474
  'agent' => client.rest_client.agent
477
475
  )
478
476
 
@@ -482,18 +480,19 @@ module Ably
482
480
  else
483
481
  'false'
484
482
  end
485
-
486
- url_params['clientId'] = client.auth.client_id if client.auth.has_client_id?
483
+ # RSA7e1
484
+ url_params['clientId'] = client.auth.client_id_for_request_sync if client.auth.client_id_for_request_sync
487
485
  url_params.merge!(client.transport_params)
488
486
 
489
- if connection_resumable?
490
- url_params.merge! resume: key, connection_serial: serial
491
- logger.debug { "Resuming connection key #{key} with serial #{serial}" }
492
- elsif connection_recoverable?
493
- url_params.merge! recover: connection_recover_parts[:recover], connectionSerial: connection_recover_parts[:connection_serial]
494
- logger.debug { "Recovering connection with key #{client.recover}" }
495
- unsafe_once(:connected, :closed, :failed) do
496
- client.disable_automatic_connection_recovery
487
+ if !key.nil_or_empty? and connection_state_available?
488
+ url_params.merge! resume: key
489
+ logger.debug { "Resuming connection with key #{key}" }
490
+ elsif !client.recover.nil_or_empty?
491
+ recovery_context = RecoveryKeyContext.from_json(client.recover, logger)
492
+ unless recovery_context.nil?
493
+ key = recovery_context.connection_key
494
+ logger.debug { "Recovering connection with key #{key}" }
495
+ url_params.merge! recover: key
497
496
  end
498
497
  end
499
498
 
@@ -544,24 +543,6 @@ module Ably
544
543
  @details = connection_details
545
544
  end
546
545
 
547
- # Executes registered callbacks for a successful connection resume event
548
- # @api private
549
- def trigger_resumed
550
- resume_callbacks.each(&:call)
551
- end
552
-
553
- # Provides a simple hook to inject a callback when a connection is successfully resumed
554
- # @api private
555
- def on_resume(&callback)
556
- resume_callbacks << callback
557
- end
558
-
559
- # Remove a registered connection resume callback
560
- # @api private
561
- def off_resume(&callback)
562
- resume_callbacks.delete(callback)
563
- end
564
-
565
546
  # Returns false if messages cannot be published as a result of message queueing being disabled
566
547
  # @api private
567
548
  def can_publish_messages?
@@ -600,6 +581,12 @@ module Ably
600
581
  @client_msg_serial = -1
601
582
  end
602
583
 
584
+ # Sets the client message serial from recover clientOption.
585
+ # @api private
586
+ def set_msg_serial_from_recover=(value)
587
+ @client_msg_serial = value
588
+ end
589
+
603
590
  # When a hearbeat or any other message from Ably is received
604
591
  # we know it's alive, see #RTN23
605
592
  # @api private
@@ -620,20 +607,12 @@ module Ably
620
607
  private
621
608
 
622
609
  # The client message serial (msgSerial) is incremented for every message that is published that requires an ACK.
623
- # Note that this is different to the connection serial that contains the last known serial number
624
- # received from the server.
625
- #
626
610
  # A message serial number does not guarantee a message has been received, only sent.
627
- # A connection serial guarantees the server has received the message and is thus used for connection recovery and resumes.
628
611
  # @return [Integer] starting at -1 indicating no messages sent, 0 when the first message is sent
629
612
  def client_msg_serial
630
613
  @client_msg_serial
631
614
  end
632
615
 
633
- def resume_callbacks
634
- @resume_callbacks ||= []
635
- end
636
-
637
616
  def create_pub_sub_message_bus
638
617
  Ably::Util::PubSub.new(
639
618
  coerce_into: lambda do |event|
@@ -665,10 +644,6 @@ module Ably
665
644
  EventMachine.next_tick { yield }
666
645
  end
667
646
 
668
- def connection_resumable?
669
- !key.nil? && !serial.nil? && connection_state_available?
670
- end
671
-
672
647
  def connection_state_available?
673
648
  return true if connected?
674
649
 
@@ -682,14 +657,6 @@ module Ably
682
657
  end
683
658
  end
684
659
 
685
- def connection_recoverable?
686
- connection_recover_parts
687
- end
688
-
689
- def connection_recover_parts
690
- client.recover.to_s.match(RECOVER_REGEX)
691
- end
692
-
693
660
  def production?
694
661
  client.environment.nil? || client.environment == :production
695
662
  end
@@ -740,3 +707,4 @@ end
740
707
  require 'ably/realtime/connection/connection_manager'
741
708
  require 'ably/realtime/connection/connection_state_machine'
742
709
  require 'ably/realtime/connection/websocket_transport'
710
+ require 'ably/realtime/recovery_key_context'
@@ -23,7 +23,7 @@ module Ably::Realtime
23
23
  :sync_starting, # Indicates the client is waiting for SYNC ProtocolMessages from Ably
24
24
  :sync_none, # Indicates the ATTACHED ProtocolMessage had no presence flag and thus no members on the channel
25
25
  :finalizing_sync,
26
- :in_sync,
26
+ :sync_complete, # Indicates completion of server initiated sync
27
27
  :failed
28
28
  )
29
29
  include Ably::Modules::StateEmitter
@@ -49,16 +49,6 @@ module Ably::Realtime
49
49
  setup_event_handlers
50
50
  end
51
51
 
52
- # When attaching to a channel that has members present, the server
53
- # initiates a sync automatically so that the client has a complete list of members.
54
- #
55
- # Until this sync is complete, this method returns false
56
- #
57
- # @return [Boolean]
58
- def sync_complete?
59
- in_sync?
60
- end
61
-
62
52
  # Update the SYNC serial from the ProtocolMessage so that SYNC can be resumed.
63
53
  # If the serial is nil, or the part after the first : is empty, then the SYNC is complete
64
54
  #
@@ -110,27 +100,27 @@ module Ably::Realtime
110
100
  # Must be defined before subsequent procs reference this callback
111
101
  reset_callbacks = nil
112
102
 
113
- in_sync_callback = lambda do
103
+ sync_complete_callback = lambda do
114
104
  reset_callbacks.call if reset_callbacks
115
105
  result_block.call
116
106
  end
117
107
 
118
- failed_callback = lambda do |error|
108
+ sync_failed_callback = lambda do |error|
119
109
  reset_callbacks.call if reset_callbacks
120
110
  deferrable.fail error
121
111
  end
122
112
 
123
113
  reset_callbacks = lambda do
124
- off(&in_sync_callback)
125
- off(&failed_callback)
126
- channel.off(&failed_callback)
114
+ off(&sync_complete_callback)
115
+ off(&sync_failed_callback)
116
+ channel.off(&sync_failed_callback)
127
117
  end
128
118
 
129
- unsafe_once(:in_sync, &in_sync_callback)
130
- unsafe_once(:failed, &failed_callback)
119
+ unsafe_once(:sync_complete, &sync_complete_callback)
120
+ unsafe_once(:failed, &sync_failed_callback)
131
121
 
132
122
  channel.unsafe_once(:detaching, :detached, :failed) do |error_reason|
133
- failed_callback.call error_reason
123
+ sync_failed_callback.call error_reason
134
124
  end
135
125
  end
136
126
 
@@ -156,12 +146,33 @@ module Ably::Realtime
156
146
  # and thus the responsibility of this library to re-enter on the channel automatically if the
157
147
  # channel loses continuity
158
148
  #
159
- # @return [Array<PresenceMessage>]
149
+ # @return [Hash<String, PresenceMessage>]
160
150
  # @api private
161
151
  def local_members
162
152
  @local_members
163
153
  end
164
154
 
155
+ def enter_local_members
156
+ local_members.values.each do |member|
157
+ logger.debug { "#{self.class.name}: Manually re-entering local presence member, client ID: #{member.client_id} with data: #{member.data}" }
158
+ presence.enter_client_with_id(member.id, member.client_id, member.data).tap do |deferrable|
159
+ deferrable.errback do |error|
160
+ re_enter_error = Ably::Models::ErrorInfo.new(
161
+ message: "unable to automatically re-enter presence channel for client_id '#{member.client_id}'. Source error code #{error.code} and message '#{error.message}'",
162
+ code: Ably::Exceptions::Codes::UNABLE_TO_AUTOMATICALLY_REENTER_PRESENCE_CHANNEL
163
+ )
164
+ channel.emit :update, Ably::Models::ChannelStateChange.new(
165
+ current: channel.state,
166
+ previous: channel.state,
167
+ event: Ably::Realtime::Channel::EVENT(:update),
168
+ reason: re_enter_error,
169
+ resumed: true
170
+ )
171
+ end
172
+ end
173
+ end
174
+ end
175
+
165
176
  private
166
177
  attr_reader :sync_session_id
167
178
 
@@ -213,23 +224,14 @@ module Ably::Realtime
213
224
  update_members_and_emit_events presence_message
214
225
  end
215
226
 
227
+ # RTP5a
216
228
  channel.unsafe_on(:failed, :detached) do
217
229
  reset_members
218
230
  reset_local_members
219
231
  end
220
232
 
221
- resume_sync_proc = method(:resume_sync).to_proc
222
-
223
233
  unsafe_on(:sync_starting) do
224
234
  @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
235
  end
234
236
 
235
237
  unsafe_on(:sync_none) do
@@ -240,61 +242,9 @@ module Ably::Realtime
240
242
 
241
243
  unsafe_on(:finalizing_sync) do
242
244
  clean_up_absent_members
243
- clean_up_members_not_present_in_sync
244
- change_state :in_sync
245
+ clean_up_members_not_present_after_sync
246
+ change_state :sync_complete
245
247
  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: Ably::Exceptions::Codes::UNABLE_TO_AUTOMATICALLY_REENTER_PRESENCE_CHANNEL
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
288
- end
289
- end
290
-
291
- # Trigger a manual SYNC operation to resume member synchronisation from last known cursor position
292
- def resume_sync
293
- connection.send_protocol_message(
294
- action: Ably::Models::ProtocolMessage::ACTION.Sync.to_i,
295
- channel: channel.name,
296
- channel_serial: sync_serial
297
- ) if channel.attached?
298
248
  end
299
249
 
300
250
  def update_members_and_emit_events(presence_message)
@@ -375,7 +325,7 @@ module Ably::Realtime
375
325
  def remove_presence_member(presence_message)
376
326
  logger.debug { "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_json}" }
377
327
 
378
- if in_sync?
328
+ if sync_complete?
379
329
  member_set_delete presence_message
380
330
  else
381
331
  member_set_upsert presence_message, false
@@ -394,17 +344,16 @@ module Ably::Realtime
394
344
  def member_set_upsert(presence_message, present)
395
345
  members[presence_message.member_key] = { present: present, message: presence_message, sync_session_id: sync_session_id }
396
346
  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" }
347
+ local_members[presence_message.client_id] = presence_message
348
+ logger.debug { "#{self.class.name}: Local member '#{presence_message.client_id}' added" }
399
349
  end
400
350
  end
401
351
 
402
352
  def member_set_delete(presence_message)
403
353
  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
354
+ if sync_complete? and presence_message.connection_id == connection.id
355
+ local_members.delete presence_message.client_id
356
+ logger.debug { "#{self.class.name}: Local member '#{presence_message.client_id}' deleted" }
408
357
  end
409
358
  end
410
359
 
@@ -431,7 +380,7 @@ module Ably::Realtime
431
380
  end
432
381
  end
433
382
 
434
- def clean_up_members_not_present_in_sync
383
+ def clean_up_members_not_present_after_sync
435
384
  members.select do |member_key, member|
436
385
  member.fetch(:sync_session_id) != sync_session_id
437
386
  end.each do |member_key, member|
@@ -19,13 +19,19 @@ module Ably::Realtime
19
19
  setup_channel_event_handlers
20
20
  end
21
21
 
22
- # Expect SYNC ProtocolMessages from the server with a list of current members on this channel
23
- #
24
- # @return [void]
25
- #
26
22
  # @api private
27
- def sync_expected
28
- presence.members.change_state :sync_starting
23
+ def on_attach(has_presence_flag)
24
+ # RTP1
25
+ if has_presence_flag
26
+ # Expect SYNC ProtocolMessages from the server with a list of current members on this channel
27
+ presence.members.change_state :sync_starting
28
+ else
29
+ # There server has indicated that there are no SYNC ProtocolMessages to come because
30
+ # there are no members on this channel
31
+ logger.debug { "#{self.class.name}: Emitting leave events for all members as a SYNC is not expected and thus there are no members on the channel" }
32
+ presence.members.change_state :sync_none
33
+ end
34
+ presence.members.enter_local_members # RTP17f
29
35
  end
30
36
 
31
37
  # Process presence messages from SYNC messages. Sync can be server-initiated or triggered following ATTACH
@@ -47,17 +53,6 @@ module Ably::Realtime
47
53
  presence.members.change_state :finalizing_sync if presence.members.sync_serial_cursor_at_end?
48
54
  end
49
55
 
50
- # There server has indicated that there are no SYNC ProtocolMessages to come because
51
- # there are no members on this channel
52
- #
53
- # @return [void]
54
- #
55
- # @api private
56
- def sync_not_expected
57
- logger.debug { "#{self.class.name}: Emitting leave events for all members as a SYNC is not expected and thus there are no members on the channel" }
58
- presence.members.change_state :sync_none
59
- end
60
-
61
56
  private
62
57
  def_delegators :presence, :members, :channel
63
58
 
@@ -110,6 +110,14 @@ module Ably::Realtime
110
110
  send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Enter, client_id, data, &success_block)
111
111
  end
112
112
 
113
+ # @api private
114
+ def enter_client_with_id(id, client_id, data = nil, &success_block)
115
+ ensure_supported_client_id client_id
116
+ ensure_supported_payload data
117
+
118
+ send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Enter, client_id, data, id, &success_block)
119
+ end
120
+
113
121
  # Leave this client from this channel. This client will be removed from the presence
114
122
  # set and presence subscribers will see a leave message for this client.
115
123
  #
@@ -338,8 +346,8 @@ module Ably::Realtime
338
346
 
339
347
  private
340
348
  # @return [Ably::Models::PresenceMessage] presence message is returned allowing callbacks to be added
341
- def send_presence_protocol_message(presence_action, client_id, data)
342
- presence_message = create_presence_message(presence_action, client_id, data)
349
+ def send_presence_protocol_message(presence_action, client_id, data, id = nil)
350
+ presence_message = create_presence_message(presence_action, client_id, data, id)
343
351
  unless presence_message.client_id
344
352
  raise Ably::Exceptions::Standard.new('Unable to enter create presence message without a client_id', 400, Ably::Exceptions::Codes::UNABLE_TO_ENTER_PRESENCE_CHANNEL_NO_CLIENTID)
345
353
  end
@@ -355,12 +363,13 @@ module Ably::Realtime
355
363
  presence_message
356
364
  end
357
365
 
358
- def create_presence_message(action, client_id, data)
366
+ def create_presence_message(action, client_id, data, id = nil)
359
367
  model = {
360
368
  action: Ably::Models::PresenceMessage.ACTION(action).to_i,
361
369
  clientId: client_id,
362
- data: data
370
+ data: data,
363
371
  }
372
+ model[:id] = id unless id.nil?
364
373
 
365
374
  Ably::Models::PresenceMessage.new(model, logger: logger).tap do |presence_message|
366
375
  presence_message.encode(client.encoders, channel.options) do |encode_error, error_message|
@@ -433,13 +442,13 @@ module Ably::Realtime
433
442
  deferrable
434
443
  end
435
444
 
436
- def send_presence_action_for_client(action, client_id, data, &success_block)
445
+ def send_presence_action_for_client(action, client_id, data, id = nil, &success_block)
437
446
  requirements_failed_deferrable = ensure_presence_publishable_on_connection_deferrable
438
447
  return requirements_failed_deferrable if requirements_failed_deferrable
439
448
 
440
449
  deferrable = create_deferrable
441
450
  ensure_channel_attached(deferrable) do
442
- send_presence_protocol_message(action, client_id, data).tap do |protocol_message|
451
+ send_presence_protocol_message(action, client_id, data, id).tap do |protocol_message|
443
452
  protocol_message.callback { |message| deferrable_succeed deferrable, &success_block }
444
453
  protocol_message.errback { |error| deferrable_fail deferrable, error }
445
454
  end