ably-pubsub-core 0.0.1.pre
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 +7 -0
- data/lib/ably/agent.rb +8 -0
- data/lib/ably/auth.rb +791 -0
- data/lib/ably/exceptions.rb +169 -0
- data/lib/ably/logger.rb +119 -0
- data/lib/ably/models/auth_details.rb +45 -0
- data/lib/ably/models/channel_details.rb +69 -0
- data/lib/ably/models/channel_metrics.rb +107 -0
- data/lib/ably/models/channel_occupancy.rb +50 -0
- data/lib/ably/models/channel_options.rb +120 -0
- data/lib/ably/models/channel_state_change.rb +84 -0
- data/lib/ably/models/channel_status.rb +63 -0
- data/lib/ably/models/cipher_params.rb +137 -0
- data/lib/ably/models/connection_details.rb +149 -0
- data/lib/ably/models/connection_state_change.rb +83 -0
- data/lib/ably/models/delta_extras.rb +28 -0
- data/lib/ably/models/device_details.rb +126 -0
- data/lib/ably/models/device_push_details.rb +94 -0
- data/lib/ably/models/error_info.rb +104 -0
- data/lib/ably/models/http_paginated_response.rb +112 -0
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +235 -0
- data/lib/ably/models/message.rb +223 -0
- data/lib/ably/models/message_encoders/base.rb +135 -0
- data/lib/ably/models/message_encoders/base64.rb +40 -0
- data/lib/ably/models/message_encoders/cipher.rb +84 -0
- data/lib/ably/models/message_encoders/json.rb +34 -0
- data/lib/ably/models/message_encoders/utf8.rb +26 -0
- data/lib/ably/models/nil_logger.rb +20 -0
- data/lib/ably/models/paginated_result.rb +201 -0
- data/lib/ably/models/presence_message.rb +244 -0
- data/lib/ably/models/protocol_message.rb +288 -0
- data/lib/ably/models/push_channel_subscription.rb +114 -0
- data/lib/ably/models/stats.rb +242 -0
- data/lib/ably/models/stats_types.rb +107 -0
- data/lib/ably/models/token_details.rb +148 -0
- data/lib/ably/models/token_request.rb +180 -0
- data/lib/ably/modules/ably.rb +26 -0
- data/lib/ably/modules/async_wrapper.rb +69 -0
- data/lib/ably/modules/channels_collection.rb +105 -0
- data/lib/ably/modules/conversions.rb +153 -0
- data/lib/ably/modules/encodeable.rb +111 -0
- data/lib/ably/modules/enum.rb +230 -0
- data/lib/ably/modules/event_emitter.rb +200 -0
- data/lib/ably/modules/event_machine_helpers.rb +26 -0
- data/lib/ably/modules/exception_codes.rb +128 -0
- data/lib/ably/modules/http_helpers.rb +41 -0
- data/lib/ably/modules/message_emitter.rb +78 -0
- data/lib/ably/modules/message_pack.rb +14 -0
- data/lib/ably/modules/model_common.rb +84 -0
- data/lib/ably/modules/safe_deferrable.rb +83 -0
- data/lib/ably/modules/safe_yield.rb +41 -0
- data/lib/ably/modules/state_emitter.rb +181 -0
- data/lib/ably/modules/state_machine.rb +59 -0
- data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
- data/lib/ably/modules/uses_state_machine.rb +111 -0
- data/lib/ably/realtime/auth.rb +283 -0
- data/lib/ably/realtime/channel/channel_manager.rb +272 -0
- data/lib/ably/realtime/channel/channel_properties.rb +40 -0
- data/lib/ably/realtime/channel/channel_state_machine.rb +104 -0
- data/lib/ably/realtime/channel/publisher.rb +82 -0
- data/lib/ably/realtime/channel/push_channel.rb +69 -0
- data/lib/ably/realtime/channel.rb +440 -0
- data/lib/ably/realtime/channels.rb +71 -0
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +225 -0
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +87 -0
- data/lib/ably/realtime/client.rb +363 -0
- data/lib/ably/realtime/connection/connection_manager.rb +594 -0
- data/lib/ably/realtime/connection/connection_state_machine.rb +144 -0
- data/lib/ably/realtime/connection/websocket_transport.rb +287 -0
- data/lib/ably/realtime/connection.rb +738 -0
- data/lib/ably/realtime/models/nil_channel.rb +30 -0
- data/lib/ably/realtime/presence/members_map.rb +395 -0
- data/lib/ably/realtime/presence/presence_manager.rb +78 -0
- data/lib/ably/realtime/presence/presence_state_machine.rb +57 -0
- data/lib/ably/realtime/presence.rb +502 -0
- data/lib/ably/realtime/push/admin.rb +75 -0
- data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
- data/lib/ably/realtime/push/device_registrations.rb +105 -0
- data/lib/ably/realtime/push.rb +25 -0
- data/lib/ably/realtime/recovery_key_context.rb +36 -0
- data/lib/ably/realtime.rb +60 -0
- data/lib/ably/rest/channel/push_channel.rb +59 -0
- data/lib/ably/rest/channel.rb +194 -0
- data/lib/ably/rest/channels.rb +47 -0
- data/lib/ably/rest/client.rb +727 -0
- data/lib/ably/rest/middleware/encoder.rb +49 -0
- data/lib/ably/rest/middleware/exceptions.rb +53 -0
- data/lib/ably/rest/middleware/external_exceptions.rb +24 -0
- data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +22 -0
- data/lib/ably/rest/middleware/logger.rb +58 -0
- data/lib/ably/rest/middleware/parse_json.rb +27 -0
- data/lib/ably/rest/middleware/parse_message_pack.rb +43 -0
- data/lib/ably/rest/presence.rb +113 -0
- data/lib/ably/rest/push/admin.rb +58 -0
- data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
- data/lib/ably/rest/push/device_registrations.rb +114 -0
- data/lib/ably/rest/push.rb +25 -0
- data/lib/ably/rest.rb +40 -0
- data/lib/ably/util/ably_extensions.rb +29 -0
- data/lib/ably/util/crypto.rb +134 -0
- data/lib/ably/util/pub_sub.rb +45 -0
- data/lib/ably/util/safe_deferrable.rb +44 -0
- data/lib/ably/version.rb +7 -0
- data/lib/ably.rb +16 -0
- metadata +497 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Ably::Realtime::Models
|
|
2
|
+
# Nil object for Channels, this object is only used within the internal API of this client library
|
|
3
|
+
# @api private
|
|
4
|
+
class NilChannel
|
|
5
|
+
include Ably::Modules::EventEmitter
|
|
6
|
+
extend Ably::Modules::Enum
|
|
7
|
+
STATE = ruby_enum('STATE', Ably::Realtime::Channel::STATE)
|
|
8
|
+
include Ably::Modules::StateEmitter
|
|
9
|
+
include Ably::Modules::UsesStateMachine
|
|
10
|
+
|
|
11
|
+
attr_reader :state_machine
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@state_machine = Ably::Realtime::Channel::ChannelStateMachine.new(self)
|
|
15
|
+
@state = STATE(state_machine.current_state)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def name
|
|
19
|
+
'Nil channel'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def __incoming_msgbus__
|
|
23
|
+
@__incoming_msgbus__ ||= Ably::Util::PubSub.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def logger
|
|
27
|
+
@logger ||= Ably::Models::NilLogger.new
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
module Ably::Realtime
|
|
2
|
+
class Presence
|
|
3
|
+
# A class encapsulating a map of the members of this presence channel,
|
|
4
|
+
# indexed by the unique {Ably::Models::PresenceMessage#member_key}
|
|
5
|
+
#
|
|
6
|
+
# This map synchronises the membership of the presence set by handling
|
|
7
|
+
# SYNC messages from the service. Since sync messages can be out-of-order -
|
|
8
|
+
# e.g. a PRESENT sync event being received after that member has in fact left -
|
|
9
|
+
# this map keeps "witness" entries, with ABSENT Action, to remember the
|
|
10
|
+
# fact that a LEAVE event has been seen for a member. These entries are
|
|
11
|
+
# cleared once the last set of updates of a sync sequence have been received.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
#
|
|
15
|
+
class MembersMap
|
|
16
|
+
include Ably::Modules::EventEmitter
|
|
17
|
+
include Ably::Modules::SafeYield
|
|
18
|
+
include Enumerable
|
|
19
|
+
extend Ably::Modules::Enum
|
|
20
|
+
|
|
21
|
+
STATE = ruby_enum('STATE',
|
|
22
|
+
:initialized,
|
|
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,
|
|
26
|
+
:sync_complete, # Indicates completion of server initiated sync
|
|
27
|
+
:failed
|
|
28
|
+
)
|
|
29
|
+
include Ably::Modules::StateEmitter
|
|
30
|
+
|
|
31
|
+
def initialize(presence)
|
|
32
|
+
@presence = presence
|
|
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
|
+
|
|
42
|
+
@absent_member_cleanup_queue = []
|
|
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
|
+
|
|
49
|
+
setup_event_handlers
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Update the SYNC serial from the ProtocolMessage so that SYNC can be resumed.
|
|
53
|
+
# If the serial is nil, or the part after the first : is empty, then the SYNC is complete
|
|
54
|
+
#
|
|
55
|
+
# @return [void]
|
|
56
|
+
#
|
|
57
|
+
# @api private
|
|
58
|
+
def update_sync_serial(serial)
|
|
59
|
+
@sync_serial = serial
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# When channel serial in ProtocolMessage SYNC is nil or
|
|
63
|
+
# an empty cursor appears after the ':' such as 'cf30e75054887:psl_7g:client:189'.
|
|
64
|
+
# That is an indication that there are no more SYNC messages.
|
|
65
|
+
#
|
|
66
|
+
# @api private
|
|
67
|
+
#
|
|
68
|
+
def sync_serial_cursor_at_end?
|
|
69
|
+
sync_serial.nil? || sync_serial.to_s.match(/^[\w-]+:?$/)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get the list of presence members
|
|
73
|
+
#
|
|
74
|
+
# @param [Hash,String] options an options Hash to filter members
|
|
75
|
+
# @option options [String] :client_id optional client_id filter for the member
|
|
76
|
+
# @option options [String] :connection_id optional connection_id filter for the member
|
|
77
|
+
# @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
|
|
78
|
+
#
|
|
79
|
+
# @yield [Array<Ably::Models::PresenceMessage>] array of present members
|
|
80
|
+
#
|
|
81
|
+
# @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
|
|
82
|
+
#
|
|
83
|
+
def get(options = {}, &block)
|
|
84
|
+
wait_for_sync = options.fetch(:wait_for_sync, true)
|
|
85
|
+
deferrable = Ably::Util::SafeDeferrable.new(logger)
|
|
86
|
+
|
|
87
|
+
result_block = lambda do
|
|
88
|
+
present_members.tap do |members|
|
|
89
|
+
members.keep_if { |member| member.connection_id == options[:connection_id] } if options[:connection_id]
|
|
90
|
+
members.keep_if { |member| member.client_id == options[:client_id] } if options[:client_id]
|
|
91
|
+
end.tap do |members|
|
|
92
|
+
safe_yield block, members if block_given?
|
|
93
|
+
deferrable.succeed members
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if !wait_for_sync || sync_complete?
|
|
98
|
+
result_block.call
|
|
99
|
+
else
|
|
100
|
+
# Must be defined before subsequent procs reference this callback
|
|
101
|
+
reset_callbacks = nil
|
|
102
|
+
|
|
103
|
+
sync_complete_callback = lambda do
|
|
104
|
+
reset_callbacks.call if reset_callbacks
|
|
105
|
+
result_block.call
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sync_failed_callback = lambda do |error|
|
|
109
|
+
reset_callbacks.call if reset_callbacks
|
|
110
|
+
deferrable.fail error
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
reset_callbacks = lambda do
|
|
114
|
+
off(&sync_complete_callback)
|
|
115
|
+
off(&sync_failed_callback)
|
|
116
|
+
channel.off(&sync_failed_callback)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
unsafe_once(:sync_complete, &sync_complete_callback)
|
|
120
|
+
unsafe_once(:failed, &sync_failed_callback)
|
|
121
|
+
|
|
122
|
+
channel.unsafe_once(:detaching, :detached, :failed) do |error_reason|
|
|
123
|
+
sync_failed_callback.call error_reason
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
deferrable
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @!attribute [r] length
|
|
131
|
+
# @return [Integer] number of present members known at this point in time, will not wait for sync operation to complete
|
|
132
|
+
def length
|
|
133
|
+
present_members.length
|
|
134
|
+
end
|
|
135
|
+
alias_method :count, :length
|
|
136
|
+
alias_method :size, :length
|
|
137
|
+
|
|
138
|
+
# Method to allow {MembersMap} to be {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
|
|
139
|
+
# @note this method will not wait for the sync operation to complete so may return an incomplete set of members. Use {MembersMap#get} instead.
|
|
140
|
+
def each(&block)
|
|
141
|
+
return to_enum(:each) unless block_given?
|
|
142
|
+
present_members.each(&block)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# A copy of the local members present i.e. members entered from this connection
|
|
146
|
+
# and thus the responsibility of this library to re-enter on the channel automatically if the
|
|
147
|
+
# channel loses continuity
|
|
148
|
+
#
|
|
149
|
+
# @return [Hash<String, PresenceMessage>]
|
|
150
|
+
# @api private
|
|
151
|
+
def local_members
|
|
152
|
+
@local_members
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def enter_local_members
|
|
156
|
+
local_members.values.each do |member|
|
|
157
|
+
logger.debug { "#{self.class.name}: Manually re-entering local presence member, client ID: #{member.client_id} with data: #{member.data}" }
|
|
158
|
+
presence.enter_client_with_id(member.id, member.client_id, member.data).tap do |deferrable|
|
|
159
|
+
deferrable.errback do |error|
|
|
160
|
+
re_enter_error = Ably::Models::ErrorInfo.new(
|
|
161
|
+
message: "unable to automatically re-enter presence channel for client_id '#{member.client_id}'. Source error code #{error.code} and message '#{error.message}'",
|
|
162
|
+
code: Ably::Exceptions::Codes::UNABLE_TO_AUTOMATICALLY_REENTER_PRESENCE_CHANNEL
|
|
163
|
+
)
|
|
164
|
+
channel.emit :update, Ably::Models::ChannelStateChange.new(
|
|
165
|
+
current: channel.state,
|
|
166
|
+
previous: channel.state,
|
|
167
|
+
event: Ably::Realtime::Channel::EVENT(:update),
|
|
168
|
+
reason: re_enter_error,
|
|
169
|
+
resumed: true
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
attr_reader :sync_session_id
|
|
178
|
+
|
|
179
|
+
def members
|
|
180
|
+
@members
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def sync_serial
|
|
184
|
+
@sync_serial
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def presence
|
|
188
|
+
@presence
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def absent_member_cleanup_queue
|
|
192
|
+
@absent_member_cleanup_queue
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def reset_members
|
|
196
|
+
@members = Hash.new
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def reset_local_members
|
|
200
|
+
@local_members = Hash.new
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def channel
|
|
204
|
+
presence.channel
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def client
|
|
208
|
+
channel.client
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def logger
|
|
212
|
+
client.logger
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def connection
|
|
216
|
+
client.connection
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def setup_event_handlers
|
|
220
|
+
presence.__incoming_msgbus__.subscribe(:presence, :sync) do |presence_message|
|
|
221
|
+
presence_message.decode(client.encoders, channel.options) do |encode_error, error_message|
|
|
222
|
+
client.logger.error error_message
|
|
223
|
+
end
|
|
224
|
+
update_members_and_emit_events presence_message
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# RTP5a
|
|
228
|
+
channel.unsafe_on(:failed, :detached) do
|
|
229
|
+
reset_members
|
|
230
|
+
reset_local_members
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
unsafe_on(:sync_starting) do
|
|
234
|
+
@sync_session_id += 1
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
unsafe_on(:sync_none) do
|
|
238
|
+
@sync_session_id += 1
|
|
239
|
+
# Immediately change to finalizing which will result in all members being cleaned up
|
|
240
|
+
change_state :finalizing_sync
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
unsafe_on(:finalizing_sync) do
|
|
244
|
+
clean_up_absent_members
|
|
245
|
+
clean_up_members_not_present_after_sync
|
|
246
|
+
change_state :sync_complete
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def update_members_and_emit_events(presence_message)
|
|
251
|
+
return unless ensure_presence_message_is_valid(presence_message)
|
|
252
|
+
|
|
253
|
+
unless should_update_member?(presence_message)
|
|
254
|
+
logger.debug { "#{self.class.name}: Skipped presence member #{presence_message.action} on channel #{presence.channel.name}.\n#{presence_message.to_json}" }
|
|
255
|
+
touch_presence_member presence_message
|
|
256
|
+
return
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
case presence_message.action
|
|
260
|
+
when Ably::Models::PresenceMessage::ACTION.Enter, Ably::Models::PresenceMessage::ACTION.Update, Ably::Models::PresenceMessage::ACTION.Present
|
|
261
|
+
add_presence_member presence_message
|
|
262
|
+
when Ably::Models::PresenceMessage::ACTION.Leave
|
|
263
|
+
remove_presence_member presence_message
|
|
264
|
+
else
|
|
265
|
+
Ably::Exceptions::ProtocolError.new("Protocol error, unknown presence action #{presence_message.action}", 400, Ably::Exceptions::Codes::PROTOCOL_ERROR)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def ensure_presence_message_is_valid(presence_message)
|
|
270
|
+
return true if presence_message.connection_id
|
|
271
|
+
|
|
272
|
+
error = Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing connectionId", 400, Ably::Exceptions::Codes::PROTOCOL_ERROR)
|
|
273
|
+
logger.error { "PresenceMap: On channel '#{channel.name}' error: #{error}" }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# If the message received is older than the last known event for presence
|
|
277
|
+
# then skip (return false). This can occur during a SYNC operation. For example:
|
|
278
|
+
# - SYNC starts
|
|
279
|
+
# - LEAVE event received for clientId 5
|
|
280
|
+
# - SYNC present even received for clientId 5 with a timestamp before LEAVE event because the LEAVE occured before the SYNC operation completed
|
|
281
|
+
#
|
|
282
|
+
# @return [Boolean] true when +new_message+ is newer than the existing member in the PresenceMap
|
|
283
|
+
#
|
|
284
|
+
def should_update_member?(new_message)
|
|
285
|
+
if members[new_message.member_key]
|
|
286
|
+
existing_message = members[new_message.member_key].fetch(:message)
|
|
287
|
+
|
|
288
|
+
# If both are messages published by clients (not fabricated), use the ID to determine newness, see #RTP2b2
|
|
289
|
+
if new_message.id.start_with?(new_message.connection_id) && existing_message.id.start_with?(existing_message.connection_id)
|
|
290
|
+
new_message_parts = new_message.id.match(/(\d+):(\d+)$/)
|
|
291
|
+
existing_message_parts = existing_message.id.match(/(\d+):(\d+)$/)
|
|
292
|
+
|
|
293
|
+
if !new_message_parts || !existing_message_parts
|
|
294
|
+
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}" }
|
|
295
|
+
return existing_message.timestamp < new_message.timestamp
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# ID is in the format "connid:msgSerial:index" such as "aaaaaa:0:0"
|
|
299
|
+
# if msgSerial is greater then the new_message should update the member
|
|
300
|
+
# if msgSerial is equal and index is greater, then update the member
|
|
301
|
+
if new_message_parts[1].to_i > existing_message_parts[1].to_i # msgSerial
|
|
302
|
+
true
|
|
303
|
+
elsif new_message_parts[1].to_i == existing_message_parts[1].to_i # msgSerial equal
|
|
304
|
+
new_message_parts[2].to_i > existing_message_parts[2].to_i # compare index
|
|
305
|
+
else
|
|
306
|
+
false
|
|
307
|
+
end
|
|
308
|
+
else
|
|
309
|
+
# This message is fabricated or could not be validated so rely on timestamps, see #RTP2b1
|
|
310
|
+
new_message.timestamp > existing_message.timestamp
|
|
311
|
+
end
|
|
312
|
+
else
|
|
313
|
+
true
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def add_presence_member(presence_message)
|
|
318
|
+
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}" }
|
|
319
|
+
# Mutate the PresenceMessage so that the action is :present, see #RTP2d
|
|
320
|
+
present_presence_message = presence_message.shallow_clone(action: Ably::Models::PresenceMessage::ACTION.Present)
|
|
321
|
+
member_set_upsert present_presence_message, true
|
|
322
|
+
presence.emit_message presence_message.action, presence_message
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def remove_presence_member(presence_message)
|
|
326
|
+
logger.debug { "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_json}" }
|
|
327
|
+
|
|
328
|
+
if sync_complete?
|
|
329
|
+
member_set_delete presence_message
|
|
330
|
+
else
|
|
331
|
+
member_set_upsert presence_message, false
|
|
332
|
+
absent_member_cleanup_queue << presence_message
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
presence.emit_message presence_message.action, presence_message
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# No update is necessary for this member as older / no change during update
|
|
339
|
+
# however we need to update the sync_session_id so that this member is not removed following SYNC
|
|
340
|
+
def touch_presence_member(presence_message)
|
|
341
|
+
members.fetch(presence_message.member_key)[:sync_session_id] = sync_session_id
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def member_set_upsert(presence_message, present)
|
|
345
|
+
members[presence_message.member_key] = { present: present, message: presence_message, sync_session_id: sync_session_id }
|
|
346
|
+
if presence_message.connection_id == connection.id
|
|
347
|
+
local_members[presence_message.client_id] = presence_message
|
|
348
|
+
logger.debug { "#{self.class.name}: Local member '#{presence_message.client_id}' added" }
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def member_set_delete(presence_message)
|
|
353
|
+
members.delete presence_message.member_key
|
|
354
|
+
if sync_complete? and presence_message.connection_id == connection.id
|
|
355
|
+
local_members.delete presence_message.client_id
|
|
356
|
+
logger.debug { "#{self.class.name}: Local member '#{presence_message.client_id}' deleted" }
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def present_members
|
|
361
|
+
members.select do |key, presence|
|
|
362
|
+
presence.fetch(:present)
|
|
363
|
+
end.map do |key, presence|
|
|
364
|
+
presence.fetch(:message)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def absent_members
|
|
369
|
+
members.reject do |key, presence|
|
|
370
|
+
presence.fetch(:present)
|
|
371
|
+
end.map do |key, presence|
|
|
372
|
+
presence.fetch(:message)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def clean_up_absent_members
|
|
377
|
+
while member_to_remove = absent_member_cleanup_queue.shift
|
|
378
|
+
logger.debug { "#{self.class.name}: Cleaning up absent member '#{member_to_remove.member_key}' after SYNC.\n#{member_to_remove.to_json}" }
|
|
379
|
+
member_set_delete member_to_remove
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def clean_up_members_not_present_after_sync
|
|
384
|
+
members.select do |member_key, member|
|
|
385
|
+
member.fetch(:sync_session_id) != sync_session_id
|
|
386
|
+
end.each do |member_key, member|
|
|
387
|
+
presence_message = member.fetch(:message).shallow_clone(action: Ably::Models::PresenceMessage::ACTION.Leave, id: nil)
|
|
388
|
+
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}" }
|
|
389
|
+
member_set_delete member.fetch(:message)
|
|
390
|
+
presence.emit_message Ably::Models::PresenceMessage::ACTION.Leave, presence_message
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Ably::Realtime
|
|
2
|
+
class Presence
|
|
3
|
+
# PresenceManager is responsible for all actions relating to presence state
|
|
4
|
+
#
|
|
5
|
+
# This is a private class and should never be used directly by developers as the API is likely to change in future.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
9
|
+
class PresenceManager
|
|
10
|
+
extend Forwardable
|
|
11
|
+
|
|
12
|
+
# {Ably::Realtime::Presence} this Manager is associated with
|
|
13
|
+
# @return [Ably::Realtime::Presence]
|
|
14
|
+
attr_reader :presence
|
|
15
|
+
|
|
16
|
+
def initialize(presence)
|
|
17
|
+
@presence = presence
|
|
18
|
+
|
|
19
|
+
setup_channel_event_handlers
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @api private
|
|
23
|
+
def on_attach(has_presence_flag)
|
|
24
|
+
# RTP1
|
|
25
|
+
if has_presence_flag
|
|
26
|
+
# Expect SYNC ProtocolMessages from the server with a list of current members on this channel
|
|
27
|
+
presence.members.change_state :sync_starting
|
|
28
|
+
else
|
|
29
|
+
# There server has indicated that there are no SYNC ProtocolMessages to come because
|
|
30
|
+
# there are no members on this channel
|
|
31
|
+
logger.debug { "#{self.class.name}: Emitting leave events for all members as a SYNC is not expected and thus there are no members on the channel" }
|
|
32
|
+
presence.members.change_state :sync_none
|
|
33
|
+
end
|
|
34
|
+
presence.members.enter_local_members # RTP17f
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Process presence messages from SYNC messages. Sync can be server-initiated or triggered following ATTACH
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
40
|
+
#
|
|
41
|
+
# @api private
|
|
42
|
+
def sync_process_messages(serial, presence_messages)
|
|
43
|
+
unless presence.members.sync_starting?
|
|
44
|
+
presence.members.change_state :sync_starting
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
presence.members.update_sync_serial serial
|
|
48
|
+
|
|
49
|
+
presence_messages.each do |presence_message|
|
|
50
|
+
presence.__incoming_msgbus__.publish :sync, presence_message
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
presence.members.change_state :finalizing_sync if presence.members.sync_serial_cursor_at_end?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
def_delegators :presence, :members, :channel
|
|
58
|
+
|
|
59
|
+
def setup_channel_event_handlers
|
|
60
|
+
channel.unsafe_on(:detached) do
|
|
61
|
+
if !presence.initialized?
|
|
62
|
+
presence.transition_state_machine :left if presence.can_transition_to?(:left)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
channel.unsafe_on(:failed) do |metadata|
|
|
67
|
+
if !presence.initialized?
|
|
68
|
+
presence.transition_state_machine :left, metadata if presence.can_transition_to?(:left)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def logger
|
|
74
|
+
presence.channel.client.logger
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'ably/modules/state_machine'
|
|
2
|
+
|
|
3
|
+
module Ably::Realtime
|
|
4
|
+
class Presence
|
|
5
|
+
# Internal class to manage presence state for {Ably::Realtime::Presence}
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
9
|
+
class PresenceStateMachine
|
|
10
|
+
include Ably::Modules::StateMachine
|
|
11
|
+
|
|
12
|
+
# States supported by this StateMachine match #{Presence::STATE}s
|
|
13
|
+
# :initialized
|
|
14
|
+
# :entering
|
|
15
|
+
# :entered
|
|
16
|
+
# :leaving
|
|
17
|
+
# :left
|
|
18
|
+
Presence::STATE.each_with_index do |state_enum, index|
|
|
19
|
+
state state_enum.to_sym, initial: index == 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Entering or entered states can skip leaving and go straight to left if a channel is detached
|
|
23
|
+
# A channel that detaches very quickly will also go straight to :left from :initialized
|
|
24
|
+
transition :from => :initialized, :to => [:entering, :left]
|
|
25
|
+
transition :from => :entering, :to => [:entered, :leaving, :left]
|
|
26
|
+
transition :from => :entered, :to => [:leaving, :left]
|
|
27
|
+
transition :from => :leaving, :to => [:left, :entering]
|
|
28
|
+
|
|
29
|
+
after_transition do |presence, transition|
|
|
30
|
+
presence.synchronize_state_with_statemachine
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
after_transition(to: [:entering]) do |presence, current_transition|
|
|
34
|
+
presence.manager.enter current_transition.metadata
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
after_transition(to: [:leaving]) do |presence, current_transition|
|
|
38
|
+
presence.manager.leave current_transition.metadata
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Transitions responsible for updating channel#error_reason
|
|
42
|
+
before_transition(to: [:left]) do |presence, current_transition|
|
|
43
|
+
presence.channel.set_channel_error_reason current_transition.metadata if is_error_type?(current_transition.metadata)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
def channel
|
|
48
|
+
object.channel
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Logged needs to be defined as it is used by {Ably::Modules::StateMachine}
|
|
52
|
+
def logger
|
|
53
|
+
channel.logger
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|