ably 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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