ably 0.6.2 → 0.7.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version.old +1 -0
  4. data/.travis.yml +0 -2
  5. data/Rakefile +22 -4
  6. data/SPEC.md +1676 -0
  7. data/ably.gemspec +1 -1
  8. data/lib/ably.rb +0 -8
  9. data/lib/ably/auth.rb +54 -46
  10. data/lib/ably/exceptions.rb +19 -5
  11. data/lib/ably/logger.rb +1 -1
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
  14. data/lib/ably/models/message.rb +15 -12
  15. data/lib/ably/models/message_encoders/base.rb +6 -5
  16. data/lib/ably/models/message_encoders/base64.rb +1 -0
  17. data/lib/ably/models/message_encoders/cipher.rb +6 -3
  18. data/lib/ably/models/message_encoders/json.rb +1 -0
  19. data/lib/ably/models/message_encoders/utf8.rb +2 -9
  20. data/lib/ably/models/nil_logger.rb +20 -0
  21. data/lib/ably/models/paginated_resource.rb +5 -2
  22. data/lib/ably/models/presence_message.rb +21 -12
  23. data/lib/ably/models/protocol_message.rb +22 -6
  24. data/lib/ably/modules/ably.rb +11 -0
  25. data/lib/ably/modules/async_wrapper.rb +2 -0
  26. data/lib/ably/modules/conversions.rb +23 -3
  27. data/lib/ably/modules/encodeable.rb +2 -1
  28. data/lib/ably/modules/enum.rb +2 -0
  29. data/lib/ably/modules/event_emitter.rb +7 -1
  30. data/lib/ably/modules/event_machine_helpers.rb +2 -0
  31. data/lib/ably/modules/http_helpers.rb +2 -0
  32. data/lib/ably/modules/model_common.rb +12 -2
  33. data/lib/ably/modules/state_emitter.rb +76 -0
  34. data/lib/ably/modules/state_machine.rb +53 -0
  35. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  36. data/lib/ably/modules/uses_state_machine.rb +74 -0
  37. data/lib/ably/realtime.rb +4 -2
  38. data/lib/ably/realtime/channel.rb +51 -58
  39. data/lib/ably/realtime/channel/channel_manager.rb +91 -0
  40. data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
  41. data/lib/ably/realtime/client.rb +70 -26
  42. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
  43. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  44. data/lib/ably/realtime/connection.rb +135 -92
  45. data/lib/ably/realtime/connection/connection_manager.rb +216 -33
  46. data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
  47. data/lib/ably/realtime/models/nil_channel.rb +10 -1
  48. data/lib/ably/realtime/presence.rb +336 -92
  49. data/lib/ably/rest.rb +2 -2
  50. data/lib/ably/rest/channel.rb +13 -4
  51. data/lib/ably/rest/client.rb +138 -38
  52. data/lib/ably/rest/middleware/logger.rb +24 -3
  53. data/lib/ably/rest/presence.rb +12 -7
  54. data/lib/ably/version.rb +1 -1
  55. data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
  56. data/spec/acceptance/realtime/channel_spec.rb +461 -120
  57. data/spec/acceptance/realtime/client_spec.rb +119 -0
  58. data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
  59. data/spec/acceptance/realtime/connection_spec.rb +571 -97
  60. data/spec/acceptance/realtime/message_spec.rb +347 -333
  61. data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
  62. data/spec/acceptance/realtime/presence_spec.rb +769 -239
  63. data/spec/acceptance/realtime/stats_spec.rb +14 -22
  64. data/spec/acceptance/realtime/time_spec.rb +16 -20
  65. data/spec/acceptance/rest/auth_spec.rb +425 -364
  66. data/spec/acceptance/rest/base_spec.rb +108 -176
  67. data/spec/acceptance/rest/channel_spec.rb +89 -89
  68. data/spec/acceptance/rest/channels_spec.rb +30 -32
  69. data/spec/acceptance/rest/client_spec.rb +273 -0
  70. data/spec/acceptance/rest/encoders_spec.rb +185 -0
  71. data/spec/acceptance/rest/message_spec.rb +186 -163
  72. data/spec/acceptance/rest/presence_spec.rb +150 -111
  73. data/spec/acceptance/rest/stats_spec.rb +45 -40
  74. data/spec/acceptance/rest/time_spec.rb +8 -10
  75. data/spec/rspec_config.rb +10 -1
  76. data/spec/shared/client_initializer_behaviour.rb +212 -0
  77. data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
  78. data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
  79. data/spec/spec_helper.rb +9 -0
  80. data/spec/support/api_helper.rb +11 -0
  81. data/spec/support/event_machine_helper.rb +101 -3
  82. data/spec/support/markdown_spec_formatter.rb +90 -0
  83. data/spec/support/private_api_formatter.rb +36 -0
  84. data/spec/support/protocol_helper.rb +32 -0
  85. data/spec/support/random_helper.rb +15 -0
  86. data/spec/support/test_app.rb +4 -0
  87. data/spec/unit/auth_spec.rb +68 -0
  88. data/spec/unit/logger_spec.rb +77 -66
  89. data/spec/unit/models/error_info_spec.rb +1 -1
  90. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
  91. data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
  92. data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
  93. data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
  94. data/spec/unit/models/message_spec.rb +160 -15
  95. data/spec/unit/models/paginated_resource_spec.rb +29 -27
  96. data/spec/unit/models/presence_message_spec.rb +163 -20
  97. data/spec/unit/models/protocol_message_spec.rb +43 -8
  98. data/spec/unit/modules/async_wrapper_spec.rb +2 -3
  99. data/spec/unit/modules/conversions_spec.rb +1 -1
  100. data/spec/unit/modules/enum_spec.rb +2 -3
  101. data/spec/unit/modules/event_emitter_spec.rb +62 -5
  102. data/spec/unit/modules/state_emitter_spec.rb +283 -0
  103. data/spec/unit/realtime/channel_spec.rb +107 -2
  104. data/spec/unit/realtime/channels_spec.rb +1 -0
  105. data/spec/unit/realtime/client_spec.rb +8 -48
  106. data/spec/unit/realtime/connection_spec.rb +3 -3
  107. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
  108. data/spec/unit/realtime/presence_spec.rb +13 -4
  109. data/spec/unit/realtime/realtime_spec.rb +0 -11
  110. data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
  111. data/spec/unit/rest/channel_spec.rb +109 -0
  112. data/spec/unit/rest/channels_spec.rb +4 -3
  113. data/spec/unit/rest/client_spec.rb +30 -125
  114. data/spec/unit/rest/rest_spec.rb +10 -0
  115. data/spec/unit/util/crypto_spec.rb +10 -5
  116. data/spec/unit/util/pub_sub_spec.rb +5 -5
  117. metadata +44 -12
  118. data/spec/integration/modules/state_emitter_spec.rb +0 -80
  119. data/spec/integration/rest/auth.rb +0 -9
@@ -1,13 +1,18 @@
1
1
  module Ably::Realtime::Models
2
2
  # Nil object for Channels, this object is only used within the internal API of this client library
3
+ # @api private
3
4
  class NilChannel
4
5
  include Ably::Modules::EventEmitter
5
6
  extend Ably::Modules::Enum
6
7
  STATE = ruby_enum('STATE', Ably::Realtime::Channel::STATE)
7
8
  include Ably::Modules::StateEmitter
9
+ include Ably::Modules::UsesStateMachine
10
+
11
+ attr_reader :state_machine
8
12
 
9
13
  def initialize
10
- @state = STATE.Initialized
14
+ @state_machine = Ably::Realtime::Channel::ChannelStateMachine.new(self)
15
+ @state = STATE(state_machine.current_state)
11
16
  end
12
17
 
13
18
  def name
@@ -17,5 +22,9 @@ module Ably::Realtime::Models
17
22
  def __incoming_msgbus__
18
23
  @__incoming_msgbus__ ||= Ably::Util::PubSub.new
19
24
  end
25
+
26
+ def logger
27
+ @logger ||= Ably::Models::NilLogger.new
28
+ end
20
29
  end
21
30
  end
@@ -16,15 +16,21 @@ module Ably::Realtime
16
16
  include Ably::Modules::StateEmitter
17
17
 
18
18
  # {Ably::Realtime::Channel} this Presence object is associated with
19
- # @return {Ably::Realtime::Channel}
19
+ # @return [Ably::Realtime::Channel]
20
20
  attr_reader :channel
21
21
 
22
- # A unique member identifier for this channel client, disambiguating situations where a given
23
- # client_id is present on multiple connections simultaneously.
24
- #
25
- # @note TODO: This does not work at present as no ACK is sent from the server with a memberId
26
- # @return {String}
27
- attr_reader :member_id
22
+ # A unique identifier for this channel client based on their connection, disambiguating situations
23
+ # where a given client_id is present on multiple connections simultaneously.
24
+ # @return [String]
25
+ attr_reader :connection_id
26
+
27
+ # The client_id for the member present on this channel
28
+ # @return [String]
29
+ attr_reader :client_id
30
+
31
+ # The data for the member present on this channel
32
+ # @return [String]
33
+ attr_reader :data
28
34
 
29
35
  def initialize(channel)
30
36
  @channel = channel
@@ -32,8 +38,6 @@ module Ably::Realtime
32
38
  @members = Hash.new
33
39
  @subscriptions = Hash.new { |hash, key| hash[key] = [] }
34
40
  @client_id = client.client_id
35
- @data = nil
36
- @member_id = nil
37
41
 
38
42
  setup_event_handlers
39
43
  end
@@ -41,102 +45,201 @@ module Ably::Realtime
41
45
  # Enter this client into this channel. This client will be added to the presence set
42
46
  # and presence subscribers will see an enter message for this client.
43
47
  #
44
- # @param [Hash,String] options an options Hash to specify client data and/or client ID, or a String with the client data
48
+ # @param [Hash] options an options Hash to specify client data and/or client ID
45
49
  # @option options [String] :data optional data (eg a status message) for this member
46
50
  # @option options [String] :client_id the optional id of the client.
47
51
  # This option is provided to support connections from server instances that act on behalf of
48
52
  # multiple client_ids. In order to be able to enter the channel with this method, the client
49
53
  # library must have been instanced either with a key, or with a token bound to the wildcard clientId.
50
54
  #
51
- # @yield [Ably::Realtime::Presence] On success, will call the block with the {Ably::Realtime::Presence}
52
- #
53
- # @return [Ably::Models::PresenceMessage] Deferrable {Ably::Models::PresenceMessage} that supports both success (callback) and failure (errback) callbacks
55
+ # @yield [Ably::Realtime::Presence] On success, will call the block with this {Ably::Realtime::Presence} object
56
+ # @return [EventMachine::Deferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
54
57
  #
55
- def enter(options = {}, &blk)
58
+ def enter(options = {}, &success_block)
56
59
  @client_id = options.fetch(:client_id, client_id)
57
60
  @data = options.fetch(:data, data)
61
+ deferrable = EventMachine::DefaultDeferrable.new
58
62
 
59
63
  raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
64
+ return deferrable_succeed(deferrable, &success_block) if state == STATE.Entered
60
65
 
61
- if state == STATE.Entered
62
- blk.call self if block_given?
63
- return
64
- end
65
-
66
- ensure_channel_attached do
67
- once(STATE.Entered) { blk.call self } if block_given?
68
-
69
- if !entering?
70
- change_state STATE.Entering
71
- send_presence_protocol_message(Ably::Models::PresenceMessage::ACTION.Enter).tap do |deferrable|
72
- deferrable.errback { |message, error| change_state STATE.Failed, error }
73
- deferrable.callback { |message| change_state STATE.Entered, message }
66
+ ensure_channel_attached(deferrable) do
67
+ if entering?
68
+ once_or_if(STATE.Entered, else: proc { |args| deferrable_fail deferrable, *args }) do
69
+ deferrable_succeed deferrable, &success_block
74
70
  end
71
+ else
72
+ change_state STATE.Entering
73
+ send_protocol_message_and_transition_state_to(
74
+ Ably::Models::PresenceMessage::ACTION.Enter,
75
+ deferrable: deferrable,
76
+ target_state: STATE.Entered,
77
+ client_id: client_id,
78
+ data: data,
79
+ failed_state: STATE.Failed,
80
+ &success_block
81
+ )
75
82
  end
76
83
  end
77
84
  end
78
85
 
86
+ # Enter the specified client_id into this channel. The given client will be added to the
87
+ # presence set and presence subscribers will see a corresponding presence message.
88
+ # This method is provided to support connections (e.g. connections from application
89
+ # server instances) that act on behalf of multiple client_ids. In order to be able to
90
+ # enter the channel with this method, the client library must have been instanced
91
+ # either with a key, or with a token bound to the wildcard client_id
92
+ #
93
+ # @param [String] client_id id of the client
94
+ #
95
+ # @param [Hash] options an options Hash for this client event
96
+ # @option options [String] :data optional data (eg a status message) for this member
97
+ #
98
+ # @yield [Ably::Realtime::Presence] On success, will call the block with this {Ably::Realtime::Presence} object
99
+ # @return [EventMachine::Deferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
100
+ #
101
+ def enter_client(client_id, options = {}, &success_block)
102
+ raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
103
+ raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
104
+
105
+ send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Enter, client_id, options, &success_block)
106
+ end
107
+
79
108
  # Leave this client from this channel. This client will be removed from the presence
80
109
  # set and presence subscribers will see a leave message for this client.
81
110
  #
82
- # @param (see Presence#enter)
111
+ # @param [Hash,String] options an options Hash to specify client data and/or client ID
112
+ # @option options [String] :data optional data (eg a status message) for this member
113
+ #
83
114
  # @yield (see Presence#enter)
84
115
  # @return (see Presence#enter)
85
116
  #
86
- def leave(options = {}, &blk)
87
- raise Ably::Exceptions::Standard.new('Unable to leave presence channel that is not entered', 400, 91002) unless ably_to_leave?
117
+ def leave(options = {}, &success_block)
118
+ @data = options.fetch(:data) if options.has_key?(:data)
119
+ deferrable = EventMachine::DefaultDeferrable.new
88
120
 
89
- @data = options.fetch(:data, data)
90
-
91
- if state == STATE.Left
92
- blk.call self if block_given?
93
- return
94
- end
95
-
96
- ensure_channel_attached do
97
- once(STATE.Left) { blk.call self } if block_given?
121
+ raise Ably::Exceptions::Standard.new('Unable to leave presence channel that is not entered', 400, 91002) unless able_to_leave?
122
+ return deferrable_succeed(deferrable, &success_block) if state == STATE.Left
98
123
 
99
- if !leaving?
100
- change_state STATE.Leaving
101
- send_presence_protocol_message(Ably::Models::PresenceMessage::ACTION.Leave).tap do |deferrable|
102
- deferrable.errback { |message, error| change_state STATE.Failed, error }
103
- deferrable.callback { |message| change_state STATE.Left }
124
+ ensure_channel_attached(deferrable) do
125
+ if leaving?
126
+ once_or_if(STATE.Left, else: proc { |error|deferrable_fail deferrable, *args }) do
127
+ deferrable_succeed deferrable, &success_block
104
128
  end
129
+ else
130
+ change_state STATE.Leaving
131
+ send_protocol_message_and_transition_state_to(
132
+ Ably::Models::PresenceMessage::ACTION.Leave,
133
+ deferrable: deferrable,
134
+ target_state: STATE.Left,
135
+ client_id: client_id,
136
+ data: data,
137
+ failed_state: STATE.Failed,
138
+ &success_block
139
+ )
105
140
  end
106
141
  end
107
142
  end
108
143
 
144
+ # Leave a given client_id from this channel. This client will be removed from the
145
+ # presence set and presence subscribers will see a leave message for this client.
146
+ #
147
+ # @param (see Presence#enter_client)
148
+ # @option options (see Presence#enter_client)
149
+ #
150
+ # @yield (see Presence#enter_client)
151
+ # @return (see Presence#enter_client)
152
+ #
153
+ def leave_client(client_id, options = {}, &success_block)
154
+ raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
155
+ raise Ably::Exceptions::Standard.new('Unable to leave presence channel without a client_id', 400, 91000) unless client_id
156
+
157
+ send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Leave, client_id, options, &success_block)
158
+ end
159
+
109
160
  # Update the presence data for this client. If the client is not already a member of
110
161
  # the presence set it will be added, and presence subscribers will see an enter or
111
162
  # update message for this client.
112
163
  #
113
- # @param (see Presence#enter)
164
+ # @param [Hash,String] options an options Hash to specify client data
165
+ # @option options [String] :data optional data (eg a status message) for this member
166
+ #
114
167
  # @yield (see Presence#enter)
115
168
  # @return (see Presence#enter)
116
169
  #
117
- def update(options = {}, &blk)
118
- @data = options.fetch(:data, data)
119
-
120
- ensure_channel_attached do
121
- send_presence_protocol_message(Ably::Models::PresenceMessage::ACTION.Update).tap do |deferrable|
122
- deferrable.callback do |message|
123
- change_state STATE.Entered, message unless entered?
124
- blk.call self if block_given?
125
- end
126
- end
170
+ def update(options = {}, &success_block)
171
+ @data = options.fetch(:data) if options.has_key?(:data)
172
+ deferrable = EventMachine::DefaultDeferrable.new
173
+
174
+ ensure_channel_attached(deferrable) do
175
+ send_protocol_message_and_transition_state_to(
176
+ Ably::Models::PresenceMessage::ACTION.Update,
177
+ deferrable: deferrable,
178
+ target_state: STATE.Entered,
179
+ client_id: client_id,
180
+ data: data,
181
+ &success_block
182
+ )
127
183
  end
128
184
  end
129
185
 
186
+ # Update the presence data for a specified client_id into this channel.
187
+ # If the client is not already a member of the presence set it will be added, and
188
+ # presence subscribers will see an enter or update message for this client.
189
+ # As with {#enter_client}, the connection must be authenticated in a way that
190
+ # enables it to represent an arbitrary clientId.
191
+ #
192
+ # @param (see Presence#enter_client)
193
+ # @option options (see Presence#enter_client)
194
+ #
195
+ # @yield (see Presence#enter_client)
196
+ # @return (see Presence#enter_client)
197
+ #
198
+ def update_client(client_id, options = {}, &success_block)
199
+ raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
200
+ raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
201
+
202
+ send_presence_action_for_client(Ably::Models::PresenceMessage::ACTION.Update, client_id, options, &success_block)
203
+ end
204
+
130
205
  # Get the presence state for this Channel.
131
- # Optionally get a member's {Ably::Models::PresenceMessage} state by member_id
132
206
  #
133
- # @return [Array<Ably::Models::PresenceMessage>, Ably::Models::PresenceMessage] members on the channel
207
+ # @param [Hash,String] options an options Hash to filter members
208
+ # @option options [String] :client_id optional client_id for the member
209
+ # @option options [String] :connection_id optional connection_id for the member
210
+ # @option options [String] :wait_for_sync defaults to true, if false the get method returns the current list of members and does not wait for the presence sync to complete
134
211
  #
135
- def get(member_id = nil)
136
- if member_id
137
- members.find { |key, presence| presence.member_id == member_id }
138
- else
139
- members.map { |key, presence| presence }
212
+ # @yield [Array<Ably::Models::PresenceMessage>] array of members or the member
213
+ #
214
+ # @return [EventMachine::Deferrable] Deferrable that supports both success (callback) and failure (errback) callback
215
+ #
216
+ def get(options = {}, &success_block)
217
+ wait_for_sync = options.fetch(:wait_for_sync, true)
218
+ deferrable = EventMachine::DefaultDeferrable.new
219
+
220
+ ensure_channel_attached(deferrable) do
221
+ result_block = proc do
222
+ members.map { |key, presence| presence }.tap do |filtered_members|
223
+ filtered_members.keep_if { |presence| presence.connection_id == options[:connection_id] } if options[:connection_id]
224
+ filtered_members.keep_if { |presence| presence.client_id == options[:client_id] } if options[:client_id]
225
+ end
226
+ end
227
+
228
+ if !wait_for_sync || sync_complete?
229
+ result = result_block.call
230
+ success_block.call result if block_given?
231
+ deferrable.succeed result
232
+ else
233
+ sync_pubsub.once(:done) do
234
+ result = result_block.call
235
+ success_block.call result if block_given?
236
+ deferrable.succeed result
237
+ end
238
+
239
+ sync_pubsub.once(:failed) do |error|
240
+ deferrable.fail error
241
+ end
242
+ end
140
243
  end
141
244
  end
142
245
 
@@ -148,9 +251,9 @@ module Ably::Realtime
148
251
  #
149
252
  # @return [void]
150
253
  #
151
- def subscribe(action = :all, &blk)
254
+ def subscribe(action = :all, &callback)
152
255
  ensure_channel_attached do
153
- subscriptions[message_action_key(action)] << blk
256
+ subscriptions[message_action_key(action)] << callback
154
257
  end
155
258
  end
156
259
 
@@ -161,14 +264,14 @@ module Ably::Realtime
161
264
  #
162
265
  # @return [void]
163
266
  #
164
- def unsubscribe(action = :all, &blk)
267
+ def unsubscribe(action = :all, &callback)
165
268
  if message_action_key(action) == :all
166
269
  subscriptions.keys
167
270
  else
168
271
  Array(message_action_key(action))
169
272
  end.each do |key|
170
273
  subscriptions[key].delete_if do |block|
171
- !block_given? || blk == block
274
+ !block_given? || callback == block
172
275
  end
173
276
  end
174
277
  end
@@ -188,6 +291,60 @@ module Ably::Realtime
188
291
  end
189
292
  end
190
293
 
294
+ # When attaching to a channel that has members present, the client and server
295
+ # initiate a sync automatically so that the client has a complete list of members.
296
+ #
297
+ # Whilst this sync is happening, this method returns false
298
+ #
299
+ # @return [Boolean]
300
+ def sync_complete?
301
+ sync_complete
302
+ end
303
+
304
+ # Expect SYNC ProtocolMessages with a list of current members on this channel from the server
305
+ #
306
+ # @return [void]
307
+ #
308
+ # @api private
309
+ def sync_started
310
+ @sync_complete = false
311
+
312
+ sync_pubsub.once(:sync_complete) do
313
+ sync_changes_backlog.each do |presence_message|
314
+ apply_member_presence_changes presence_message
315
+ end
316
+ sync_completed
317
+ sync_pubsub.trigger :done
318
+ end
319
+
320
+ channel.once_or_if [:detached, :failed] do |error|
321
+ sync_completed
322
+ sync_pubsub.trigger :failed, error
323
+ end
324
+ end
325
+
326
+ # The server has indicated that no members are present on this channel and no SYNC is expected,
327
+ # or that the SYNC has now completed
328
+ #
329
+ # @return [void]
330
+ #
331
+ # @api private
332
+ def sync_completed
333
+ @sync_complete = true
334
+ @sync_changes_backlog = []
335
+ end
336
+
337
+ # Update the SYNC serial from the ProtocolMessage so that SYNC can be resumed.
338
+ # If the serial is nil, or the part after the first : is empty, then the SYNC is complete
339
+ #
340
+ # @return [void]
341
+ #
342
+ # @api private
343
+ def update_sync_serial(serial)
344
+ @sync_serial = serial
345
+ sync_pubsub.trigger :sync_complete if sync_serial_cursor_at_end?
346
+ end
347
+
191
348
  # @!attribute [r] __incoming_msgbus__
192
349
  # @return [Ably::Util::PubSub] Client library internal channel incoming message bus
193
350
  # @api private
@@ -198,19 +355,34 @@ module Ably::Realtime
198
355
  end
199
356
 
200
357
  private
201
- attr_reader :members, :subscriptions, :client_id, :data
358
+ attr_reader :members, :subscriptions, :sync_serial, :sync_complete
202
359
 
203
- def ably_to_leave?
360
+
361
+ # A simple PubSub class used to publish synchronisation state changes
362
+ def sync_pubsub
363
+ @sync_pubsub ||= Ably::Util::PubSub.new
364
+ end
365
+
366
+ # During a SYNC of presence members, all enter, update and leave events are queued for processing once the SYNC is complete
367
+ def sync_changes_backlog
368
+ @sync_changes_backlog ||= []
369
+ end
370
+
371
+ # When channel serial in ProtocolMessage SYNC is nil or
372
+ # an empty cursor appears after the ':' such as 'cf30e75054887:psl_7g:client:189'
373
+ # then there are no more SYNC messages to come
374
+ def sync_serial_cursor_at_end?
375
+ sync_serial.nil? || sync_serial.to_s.match(/^[\w-]+:?$/)
376
+ end
377
+
378
+ def able_to_leave?
204
379
  entering? || entered?
205
380
  end
206
381
 
207
382
  def setup_event_handlers
208
- __incoming_msgbus__.subscribe(:presence) do |presence|
209
- presence.decode self.channel
210
- update_members_from_presence_message presence
211
-
212
- subscriptions[:all].each { |cb| cb.call(presence) }
213
- subscriptions[presence.action].each { |cb| cb.call(presence) }
383
+ __incoming_msgbus__.subscribe(:presence, :sync) do |presence_message|
384
+ presence_message.decode self.channel
385
+ update_members_from_presence_message presence_message
214
386
  end
215
387
 
216
388
  channel.on(Channel::STATE.Detaching) do
@@ -226,13 +398,13 @@ module Ably::Realtime
226
398
  end
227
399
 
228
400
  on(STATE.Entered) do |message|
229
- @member_id = message.member_id
401
+ @connection_id = message.connection_id
230
402
  end
231
403
  end
232
404
 
233
405
  # @return [Ably::Models::PresenceMessage] presence message is returned allowing callbacks to be added
234
- def send_presence_protocol_message(presence_action)
235
- presence_message = create_presence_message(presence_action)
406
+ def send_presence_protocol_message(presence_action, client_id, options = {})
407
+ presence_message = create_presence_message(presence_action, client_id, options)
236
408
  unless presence_message.client_id
237
409
  raise Ably::Exceptions::Standard.new('Unable to enter create presence message without a client_id', 400, 91000)
238
410
  end
@@ -248,12 +420,12 @@ module Ably::Realtime
248
420
  presence_message
249
421
  end
250
422
 
251
- def create_presence_message(action)
423
+ def create_presence_message(action, client_id, options = {})
252
424
  model = {
253
425
  action: Ably::Models::PresenceMessage.ACTION(action).to_i,
254
- clientId: client_id,
426
+ clientId: client_id
255
427
  }
256
- model.merge!(data: data) if data
428
+ model.merge!(data: options.fetch(:data)) if options.has_key?(:data)
257
429
 
258
430
  Ably::Models::PresenceMessage.new(model, nil).tap do |presence_message|
259
431
  presence_message.encode self.channel
@@ -261,31 +433,103 @@ module Ably::Realtime
261
433
  end
262
434
 
263
435
  def update_members_from_presence_message(presence_message)
264
- unless presence_message.member_id
265
- new Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing memberId", 400, 80013)
436
+ unless presence_message.connection_id
437
+ Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing connectionId", 400, 80013)
266
438
  end
267
439
 
268
- case presence_message.action
269
- when Ably::Models::PresenceMessage::ACTION.Enter
270
- members[presence_message.member_id] = presence_message
271
-
272
- when Ably::Models::PresenceMessage::ACTION.Update
273
- members[presence_message.member_id] = presence_message
440
+ if sync_complete?
441
+ apply_member_presence_changes presence_message
442
+ else
443
+ if presence_message.action == Ably::Models::PresenceMessage::ACTION.Present
444
+ add_presence_member presence_message
445
+ publish_presence_member_state_change presence_message
446
+ else
447
+ sync_changes_backlog << presence_message
448
+ end
449
+ end
450
+ end
274
451
 
452
+ def apply_member_presence_changes(presence_message)
453
+ case presence_message.action
454
+ when Ably::Models::PresenceMessage::ACTION.Enter, Ably::Models::PresenceMessage::ACTION.Update
455
+ add_presence_member presence_message
275
456
  when Ably::Models::PresenceMessage::ACTION.Leave
276
- members.delete presence_message.member_id
277
-
457
+ remove_presence_member presence_message
278
458
  else
279
- new Ably::Exceptions::ProtocolError.new("Protocol error, unknown presence action #{presence.action}", 400, 80013)
459
+ Ably::Exceptions::ProtocolError.new("Protocol error, unknown presence action #{presence_message.action}", 400, 80013)
280
460
  end
461
+
462
+ publish_presence_member_state_change presence_message
463
+ end
464
+
465
+ def add_presence_member(presence_message)
466
+ members[presence_message.member_key] = presence_message
281
467
  end
282
468
 
283
- def ensure_channel_attached
469
+ def remove_presence_member(presence_message)
470
+ members.delete presence_message.member_key
471
+ end
472
+
473
+ def publish_presence_member_state_change(presence_message)
474
+ subscriptions[:all].each { |cb| cb.call(presence_message) }
475
+ subscriptions[presence_message.action].each { |cb| cb.call(presence_message) }
476
+ end
477
+
478
+ def ensure_channel_attached(deferrable = nil)
284
479
  if channel.attached?
285
480
  yield
286
481
  else
287
482
  attach_channel_then { yield }
288
483
  end
484
+ deferrable
485
+ end
486
+
487
+ def send_protocol_message_and_transition_state_to(action, options = {}, &success_block)
488
+ deferrable = options.fetch(:deferrable) { raise ArgumentError, 'option :deferrable is required' }
489
+ client_id = options.fetch(:client_id) { raise ArgumentError, 'option :client_id is required' }
490
+ target_state = options.fetch(:target_state, nil)
491
+ failed_state = options.fetch(:failed_state, nil)
492
+
493
+ protocol_message_options = if options.has_key?(:data)
494
+ { data: options.fetch(:data) }
495
+ else
496
+ { }
497
+ end
498
+
499
+ send_presence_protocol_message(action, client_id, protocol_message_options).tap do |protocol_message|
500
+ protocol_message.callback do |message|
501
+ change_state target_state, message if target_state
502
+ deferrable_succeed deferrable, &success_block
503
+ end
504
+
505
+ protocol_message.errback do |message, error|
506
+ change_state failed_state, error if failed_state
507
+ deferrable_fail deferrable, error
508
+ end
509
+ end
510
+ end
511
+
512
+ def deferrable_succeed(deferrable, *args, &block)
513
+ block.call self, *args if block_given?
514
+ EventMachine.next_tick { deferrable.succeed self, *args } # allow callback to be added to the returned Deferrable
515
+ deferrable
516
+ end
517
+
518
+ def deferrable_fail(deferrable, *args, &block)
519
+ block.call self, *args if block_given?
520
+ EventMachine.next_tick { deferrable.fail self, *args } # allow errback to be added to the returned Deferrable
521
+ deferrable
522
+ end
523
+
524
+ def send_presence_action_for_client(action, client_id, options = {}, &success_block)
525
+ deferrable = EventMachine::DefaultDeferrable.new
526
+
527
+ ensure_channel_attached(deferrable) do
528
+ send_presence_protocol_message(action, client_id, options).tap do |protocol_message|
529
+ protocol_message.callback { |message| deferrable_succeed deferrable, &success_block }
530
+ protocol_message.errback { |message| deferrable_fail deferrable }
531
+ end
532
+ end
289
533
  end
290
534
 
291
535
  def attach_channel_then