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