ably 0.8.15 → 1.0.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.
- checksums.yaml +4 -4
- data/.travis.yml +6 -4
- data/CHANGELOG.md +6 -2
- data/README.md +5 -1
- data/SPEC.md +1473 -852
- data/ably.gemspec +11 -8
- data/lib/ably/auth.rb +90 -53
- data/lib/ably/exceptions.rb +37 -8
- data/lib/ably/logger.rb +10 -1
- data/lib/ably/models/auth_details.rb +42 -0
- data/lib/ably/models/channel_state_change.rb +18 -4
- data/lib/ably/models/connection_details.rb +6 -3
- data/lib/ably/models/connection_state_change.rb +4 -3
- data/lib/ably/models/error_info.rb +1 -1
- data/lib/ably/models/message.rb +17 -1
- data/lib/ably/models/message_encoders/base.rb +103 -82
- data/lib/ably/models/message_encoders/base64.rb +1 -1
- data/lib/ably/models/presence_message.rb +16 -1
- data/lib/ably/models/protocol_message.rb +20 -3
- data/lib/ably/models/token_details.rb +11 -1
- data/lib/ably/models/token_request.rb +16 -6
- data/lib/ably/modules/async_wrapper.rb +7 -3
- data/lib/ably/modules/encodeable.rb +51 -12
- data/lib/ably/modules/enum.rb +17 -7
- data/lib/ably/modules/event_emitter.rb +29 -14
- data/lib/ably/modules/model_common.rb +13 -21
- data/lib/ably/modules/state_emitter.rb +7 -4
- data/lib/ably/modules/state_machine.rb +2 -4
- data/lib/ably/modules/uses_state_machine.rb +7 -3
- data/lib/ably/realtime.rb +2 -0
- data/lib/ably/realtime/auth.rb +102 -42
- data/lib/ably/realtime/channel.rb +68 -26
- data/lib/ably/realtime/channel/channel_manager.rb +154 -65
- data/lib/ably/realtime/channel/channel_state_machine.rb +14 -15
- data/lib/ably/realtime/client.rb +18 -3
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +38 -29
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +6 -1
- data/lib/ably/realtime/connection.rb +108 -49
- data/lib/ably/realtime/connection/connection_manager.rb +167 -61
- data/lib/ably/realtime/connection/connection_state_machine.rb +22 -3
- data/lib/ably/realtime/connection/websocket_transport.rb +19 -10
- data/lib/ably/realtime/presence.rb +70 -45
- data/lib/ably/realtime/presence/members_map.rb +201 -36
- data/lib/ably/realtime/presence/presence_manager.rb +30 -6
- data/lib/ably/realtime/presence/presence_state_machine.rb +5 -12
- data/lib/ably/rest.rb +2 -2
- data/lib/ably/rest/channel.rb +5 -5
- data/lib/ably/rest/client.rb +31 -27
- data/lib/ably/rest/middleware/exceptions.rb +1 -3
- data/lib/ably/rest/middleware/logger.rb +2 -2
- data/lib/ably/rest/presence.rb +2 -2
- data/lib/ably/util/pub_sub.rb +1 -1
- data/lib/ably/util/safe_deferrable.rb +26 -0
- data/lib/ably/version.rb +2 -2
- data/spec/acceptance/realtime/auth_spec.rb +470 -111
- data/spec/acceptance/realtime/channel_history_spec.rb +5 -3
- data/spec/acceptance/realtime/channel_spec.rb +1017 -168
- data/spec/acceptance/realtime/client_spec.rb +6 -6
- data/spec/acceptance/realtime/connection_failures_spec.rb +458 -27
- data/spec/acceptance/realtime/connection_spec.rb +424 -105
- data/spec/acceptance/realtime/message_spec.rb +52 -23
- data/spec/acceptance/realtime/presence_history_spec.rb +5 -3
- data/spec/acceptance/realtime/presence_spec.rb +1110 -96
- data/spec/acceptance/rest/auth_spec.rb +222 -59
- data/spec/acceptance/rest/base_spec.rb +1 -1
- data/spec/acceptance/rest/channel_spec.rb +1 -2
- data/spec/acceptance/rest/client_spec.rb +104 -48
- data/spec/acceptance/rest/message_spec.rb +42 -15
- data/spec/acceptance/rest/presence_spec.rb +4 -11
- data/spec/rspec_config.rb +2 -1
- data/spec/shared/client_initializer_behaviour.rb +2 -2
- data/spec/shared/safe_deferrable_behaviour.rb +6 -2
- data/spec/spec_helper.rb +4 -2
- data/spec/support/debug_failure_helper.rb +20 -4
- data/spec/support/event_machine_helper.rb +32 -1
- data/spec/unit/auth_spec.rb +4 -11
- data/spec/unit/logger_spec.rb +28 -2
- data/spec/unit/models/auth_details_spec.rb +49 -0
- data/spec/unit/models/channel_state_change_spec.rb +23 -3
- data/spec/unit/models/connection_details_spec.rb +12 -1
- data/spec/unit/models/connection_state_change_spec.rb +15 -4
- data/spec/unit/models/message_encoders/base64_spec.rb +2 -1
- data/spec/unit/models/message_spec.rb +153 -0
- data/spec/unit/models/presence_message_spec.rb +192 -0
- data/spec/unit/models/protocol_message_spec.rb +64 -6
- data/spec/unit/models/token_details_spec.rb +75 -0
- data/spec/unit/models/token_request_spec.rb +74 -0
- data/spec/unit/modules/async_wrapper_spec.rb +2 -1
- data/spec/unit/modules/enum_spec.rb +69 -0
- data/spec/unit/modules/event_emitter_spec.rb +149 -22
- data/spec/unit/modules/state_emitter_spec.rb +9 -3
- data/spec/unit/realtime/client_spec.rb +1 -1
- data/spec/unit/realtime/connection_spec.rb +8 -5
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +1 -1
- data/spec/unit/realtime/presence_spec.rb +4 -3
- data/spec/unit/rest/client_spec.rb +1 -1
- data/spec/unit/util/crypto_spec.rb +3 -3
- metadata +22 -19
@@ -47,8 +47,7 @@ module Ably::Realtime
|
|
47
47
|
after_transition(to: [:connected]) do |connection, current_transition|
|
48
48
|
error = current_transition.metadata.reason
|
49
49
|
if is_error_type?(error)
|
50
|
-
connection.logger.warn "ConnectionManager: Connected with error - #{error.message}"
|
51
|
-
connection.emit :error, error
|
50
|
+
connection.logger.warn { "ConnectionManager: Connected with error - #{error.message}" }
|
52
51
|
end
|
53
52
|
end
|
54
53
|
|
@@ -62,6 +61,11 @@ module Ably::Realtime
|
|
62
61
|
connection.manager.respond_to_transport_disconnected_whilst_connected err
|
63
62
|
end
|
64
63
|
|
64
|
+
after_transition(to: [:suspended]) do |connection, current_transition|
|
65
|
+
err = error_from_state_change(current_transition)
|
66
|
+
connection.manager.suspend_active_channels err
|
67
|
+
end
|
68
|
+
|
65
69
|
after_transition(to: [:disconnected, :suspended]) do |connection|
|
66
70
|
connection.manager.destroy_transport # never reuse a transport if the connection has failed
|
67
71
|
end
|
@@ -71,6 +75,18 @@ module Ably::Realtime
|
|
71
75
|
connection.manager.fail err
|
72
76
|
end
|
73
77
|
|
78
|
+
after_transition(to: [:failed]) do |connection, current_transition|
|
79
|
+
err = error_from_state_change(current_transition)
|
80
|
+
connection.manager.fail_active_channels err
|
81
|
+
end
|
82
|
+
|
83
|
+
# RTN7C - If a connection enters the SUSPENDED, CLOSED or FAILED state...
|
84
|
+
# the client should consider the delivery of those messages as failed
|
85
|
+
after_transition(to: [:suspended, :closed, :failed]) do |connection, current_transition|
|
86
|
+
err = error_from_state_change(current_transition)
|
87
|
+
connection.manager.nack_messages_on_all_channels err
|
88
|
+
end
|
89
|
+
|
74
90
|
after_transition(to: [:closing], from: [:initialized, :disconnected, :suspended]) do |connection|
|
75
91
|
connection.manager.force_close_connection
|
76
92
|
end
|
@@ -83,6 +99,10 @@ module Ably::Realtime
|
|
83
99
|
connection.manager.destroy_transport
|
84
100
|
end
|
85
101
|
|
102
|
+
after_transition(to: [:closed]) do |connection|
|
103
|
+
connection.manager.detach_active_channels
|
104
|
+
end
|
105
|
+
|
86
106
|
# Transitions responsible for updating connection#error_reason
|
87
107
|
before_transition(to: [:disconnected, :suspended, :failed]) do |connection, current_transition|
|
88
108
|
err = error_from_state_change(current_transition)
|
@@ -91,7 +111,6 @@ module Ably::Realtime
|
|
91
111
|
|
92
112
|
before_transition(to: [:connected, :closed]) do |connection, current_transition|
|
93
113
|
err = error_from_state_change(current_transition)
|
94
|
-
|
95
114
|
if err
|
96
115
|
connection.set_failed_connection_error_reason err
|
97
116
|
else
|
@@ -119,14 +119,14 @@ module Ably::Realtime
|
|
119
119
|
when :msgpack
|
120
120
|
driver.binary(object.to_msgpack.unpack('C*'))
|
121
121
|
else
|
122
|
-
client.logger.fatal "WebsocketTransport: Unsupported protocol '#{client.protocol}' for serialization, object cannot be serialized and sent to Ably over this WebSocket"
|
122
|
+
client.logger.fatal { "WebsocketTransport: Unsupported protocol '#{client.protocol}' for serialization, object cannot be serialized and sent to Ably over this WebSocket" }
|
123
123
|
end
|
124
124
|
end
|
125
125
|
|
126
126
|
def setup_event_handlers
|
127
127
|
__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |protocol_message|
|
128
128
|
send_object protocol_message
|
129
|
-
client.logger.debug "WebsocketTransport: Prot msg sent =>: #{protocol_message.action} #{protocol_message}"
|
129
|
+
client.logger.debug { "WebsocketTransport: Prot msg sent =>: #{protocol_message.action} #{protocol_message}" }
|
130
130
|
end
|
131
131
|
end
|
132
132
|
|
@@ -147,32 +147,41 @@ module Ably::Realtime
|
|
147
147
|
@driver = WebSocket::Driver.client(self)
|
148
148
|
|
149
149
|
driver.on("open") do
|
150
|
-
logger.debug "WebsocketTransport: socket opened to #{url}, waiting for Connected protocol message"
|
150
|
+
logger.debug { "WebsocketTransport: socket opened to #{url}, waiting for Connected protocol message" }
|
151
151
|
end
|
152
152
|
|
153
153
|
driver.on("message") do |event|
|
154
154
|
event_data = parse_event_data(event.data).freeze
|
155
155
|
protocol_message = Ably::Models::ProtocolMessage.new(event_data, logger: logger)
|
156
156
|
action_name = Ably::Models::ProtocolMessage::ACTION[event_data['action']] rescue event_data['action']
|
157
|
-
logger.debug "WebsocketTransport: Prot msg recv <=: #{action_name} - #{event_data}"
|
157
|
+
logger.debug { "WebsocketTransport: Prot msg recv <=: #{action_name} - #{event_data}" }
|
158
158
|
|
159
159
|
if protocol_message.invalid?
|
160
|
-
error = Ably::Exceptions::ProtocolError.new("Invalid Protocol Message received: #{event_data}\
|
161
|
-
|
162
|
-
|
160
|
+
error = Ably::Exceptions::ProtocolError.new("Invalid Protocol Message received: #{event_data}\nConnection moving to the failed state as the protocol is invalid and unsupported", 400, 80013)
|
161
|
+
logger.fatal { "WebsocketTransport: #{error.message}" }
|
162
|
+
failed_protocol_message = Ably::Models::ProtocolMessage.new(
|
163
|
+
action: Ably::Models::ProtocolMessage::ACTION.Error,
|
164
|
+
error: error.as_json,
|
165
|
+
logger: logger
|
166
|
+
)
|
167
|
+
__incoming_protocol_msgbus__.publish :protocol_message, failed_protocol_message
|
163
168
|
else
|
164
169
|
__incoming_protocol_msgbus__.publish :protocol_message, protocol_message
|
165
170
|
end
|
166
171
|
end
|
167
172
|
|
173
|
+
driver.on("ping") do
|
174
|
+
__incoming_protocol_msgbus__.publish :protocol_message, Ably::Models::ProtocolMessage.new(action: Ably::Models::ProtocolMessage::ACTION.Heartbeat, source: :websocket)
|
175
|
+
end
|
176
|
+
|
168
177
|
driver.on("error") do |error|
|
169
|
-
logger.error "WebsocketTransport: Protocol Error on transports - #{error.message}"
|
178
|
+
logger.error { "WebsocketTransport: Protocol Error on transports - #{error.message}" }
|
170
179
|
end
|
171
180
|
|
172
181
|
@reason_closed = nil
|
173
182
|
driver.on("closed") do |event|
|
174
183
|
@reason_closed = "#{event.code}: #{event.reason}"
|
175
|
-
logger.warn "WebsocketTransport: Driver reported transport as closed - #{reason_closed}"
|
184
|
+
logger.warn { "WebsocketTransport: Driver reported transport as closed - #{reason_closed}" }
|
176
185
|
end
|
177
186
|
end
|
178
187
|
|
@@ -192,7 +201,7 @@ module Ably::Realtime
|
|
192
201
|
when :msgpack
|
193
202
|
MessagePack.unpack(data.pack('C*'))
|
194
203
|
else
|
195
|
-
client.logger.fatal "WebsocketTransport: Unsupported Protocol Message format #{client.protocol}"
|
204
|
+
client.logger.fatal { "WebsocketTransport: Unsupported Protocol Message format #{client.protocol}" }
|
196
205
|
data
|
197
206
|
end
|
198
207
|
end
|
@@ -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)
|
@@ -346,34 +352,42 @@ module Ably::Realtime
|
|
346
352
|
}
|
347
353
|
|
348
354
|
Ably::Models::PresenceMessage.new(model, logger: logger).tap do |presence_message|
|
349
|
-
presence_message.encode
|
355
|
+
presence_message.encode(client.encoders, channel.options) do |encode_error, error_message|
|
356
|
+
client.logger.error error_message
|
357
|
+
end
|
350
358
|
end
|
351
359
|
end
|
352
360
|
|
353
|
-
def
|
361
|
+
def ensure_presence_publishable_on_connection_deferrable
|
354
362
|
if !connection.can_publish_messages?
|
355
|
-
|
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)
|
356
365
|
end
|
357
366
|
end
|
358
367
|
|
359
|
-
def ensure_channel_attached(deferrable = nil)
|
368
|
+
def ensure_channel_attached(deferrable = nil, options = {})
|
360
369
|
if channel.attached?
|
361
370
|
yield
|
371
|
+
elsif options[:allow_suspended] && channel.suspended?
|
372
|
+
yield
|
362
373
|
else
|
363
|
-
attach_channel_then { yield }
|
374
|
+
attach_channel_then(deferrable) { yield }
|
364
375
|
end
|
365
376
|
deferrable
|
366
377
|
end
|
367
378
|
|
368
379
|
def ensure_supported_client_id(check_client_id)
|
369
380
|
unless check_client_id
|
370
|
-
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')
|
371
382
|
end
|
372
383
|
if check_client_id == '*'
|
373
|
-
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')
|
374
388
|
end
|
375
389
|
unless client.auth.can_assume_client_id?(check_client_id)
|
376
|
-
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}'")
|
377
391
|
end
|
378
392
|
end
|
379
393
|
|
@@ -409,7 +423,8 @@ module Ably::Realtime
|
|
409
423
|
end
|
410
424
|
|
411
425
|
def send_presence_action_for_client(action, client_id, data, &success_block)
|
412
|
-
|
426
|
+
requirements_failed_deferrable = ensure_presence_publishable_on_connection_deferrable
|
427
|
+
return requirements_failed_deferrable if requirements_failed_deferrable
|
413
428
|
|
414
429
|
deferrable = create_deferrable
|
415
430
|
ensure_channel_attached(deferrable) do
|
@@ -420,15 +435,25 @@ module Ably::Realtime
|
|
420
435
|
end
|
421
436
|
end
|
422
437
|
|
423
|
-
def attach_channel_then
|
438
|
+
def attach_channel_then(deferrable)
|
424
439
|
if channel.detached? || channel.failed?
|
425
|
-
|
440
|
+
deferrable.fail Ably::Exceptions::InvalidState.new("Operation is not allowed when channel is in #{channel.state}", 400, 91001)
|
426
441
|
else
|
427
|
-
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
|
428
449
|
channel.attach
|
429
450
|
end
|
430
451
|
end
|
431
452
|
|
453
|
+
def implicit_attach
|
454
|
+
channel.attach if channel.initialized?
|
455
|
+
end
|
456
|
+
|
432
457
|
def client
|
433
458
|
channel.client
|
434
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
|
@@ -165,18 +207,84 @@ module Ably::Realtime
|
|
165
207
|
|
166
208
|
def setup_event_handlers
|
167
209
|
presence.__incoming_msgbus__.subscribe(:presence, :sync) do |presence_message|
|
168
|
-
presence_message.decode channel
|
210
|
+
presence_message.decode(client.encoders, channel.options) do |encode_error, error_message|
|
211
|
+
client.logger.error error_message
|
212
|
+
end
|
169
213
|
update_members_and_emit_events presence_message
|
170
214
|
end
|
171
215
|
|
216
|
+
channel.unsafe_on(:failed, :detached) do
|
217
|
+
reset_members
|
218
|
+
reset_local_members
|
219
|
+
end
|
220
|
+
|
172
221
|
resume_sync_proc = method(:resume_sync).to_proc
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
233
|
+
end
|
234
|
+
|
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
|
176
239
|
end
|
177
240
|
|
178
|
-
|
241
|
+
unsafe_on(:finalizing_sync) do
|
179
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
|
180
288
|
end
|
181
289
|
end
|
182
290
|
|
@@ -186,21 +294,15 @@ module Ably::Realtime
|
|
186
294
|
action: Ably::Models::ProtocolMessage::ACTION.Sync.to_i,
|
187
295
|
channel: channel.name,
|
188
296
|
channel_serial: sync_serial
|
189
|
-
)
|
190
|
-
end
|
191
|
-
|
192
|
-
# When channel serial in ProtocolMessage SYNC is nil or
|
193
|
-
# an empty cursor appears after the ':' such as 'cf30e75054887:psl_7g:client:189'.
|
194
|
-
# That is an indication that there are no more SYNC messages.
|
195
|
-
def sync_serial_cursor_at_end?
|
196
|
-
sync_serial.nil? || sync_serial.to_s.match(/^[\w-]+:?$/)
|
297
|
+
) if channel.attached?
|
197
298
|
end
|
198
299
|
|
199
300
|
def update_members_and_emit_events(presence_message)
|
200
301
|
return unless ensure_presence_message_is_valid(presence_message)
|
201
302
|
|
202
303
|
unless should_update_member?(presence_message)
|
203
|
-
logger.debug "#{self.class.name}: Skipped presence member #{presence_message.action} on channel #{presence.channel.name}.\n#{presence_message.
|
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
|
204
306
|
return
|
205
307
|
end
|
206
308
|
|
@@ -218,45 +320,94 @@ module Ably::Realtime
|
|
218
320
|
return true if presence_message.connection_id
|
219
321
|
|
220
322
|
error = Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing connectionId", 400, 80013)
|
221
|
-
logger.error "PresenceMap: On channel '#{channel.name}' error: #{error}"
|
222
|
-
channel.emit :error, error
|
323
|
+
logger.error { "PresenceMap: On channel '#{channel.name}' error: #{error}" }
|
223
324
|
end
|
224
325
|
|
225
326
|
# If the message received is older than the last known event for presence
|
226
|
-
# then skip.
|
327
|
+
# then skip (return false). This can occur during a SYNC operation. For example:
|
227
328
|
# - SYNC starts
|
228
329
|
# - LEAVE event received for clientId 5
|
229
330
|
# - SYNC present even received for clientId 5 with a timestamp before LEAVE event because the LEAVE occured before the SYNC operation completed
|
230
331
|
#
|
231
|
-
# @return [Boolean]
|
332
|
+
# @return [Boolean] true when +new_message+ is newer than the existing member in the PresenceMap
|
232
333
|
#
|
233
|
-
def should_update_member?(
|
234
|
-
if members[
|
235
|
-
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
|
236
362
|
else
|
237
363
|
true
|
238
364
|
end
|
239
365
|
end
|
240
366
|
|
241
367
|
def add_presence_member(presence_message)
|
242
|
-
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.
|
243
|
-
|
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
|
244
372
|
presence.emit_message presence_message.action, presence_message
|
245
373
|
end
|
246
374
|
|
247
375
|
def remove_presence_member(presence_message)
|
248
|
-
logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.
|
376
|
+
logger.debug { "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_json}" }
|
249
377
|
|
250
378
|
if in_sync?
|
251
|
-
|
379
|
+
member_set_delete presence_message
|
252
380
|
else
|
253
|
-
|
254
|
-
absent_member_cleanup_queue << presence_message
|
381
|
+
member_set_upsert presence_message, false
|
382
|
+
absent_member_cleanup_queue << presence_message
|
255
383
|
end
|
256
384
|
|
257
385
|
presence.emit_message presence_message.action, presence_message
|
258
386
|
end
|
259
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
|
+
|
260
411
|
def present_members
|
261
412
|
members.select do |key, presence|
|
262
413
|
presence.fetch(:present)
|
@@ -274,7 +425,21 @@ module Ably::Realtime
|
|
274
425
|
end
|
275
426
|
|
276
427
|
def clean_up_absent_members
|
277
|
-
|
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
|
278
443
|
end
|
279
444
|
end
|
280
445
|
end
|