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.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ably/agent.rb +8 -0
  3. data/lib/ably/auth.rb +791 -0
  4. data/lib/ably/exceptions.rb +169 -0
  5. data/lib/ably/logger.rb +119 -0
  6. data/lib/ably/models/auth_details.rb +45 -0
  7. data/lib/ably/models/channel_details.rb +69 -0
  8. data/lib/ably/models/channel_metrics.rb +107 -0
  9. data/lib/ably/models/channel_occupancy.rb +50 -0
  10. data/lib/ably/models/channel_options.rb +120 -0
  11. data/lib/ably/models/channel_state_change.rb +84 -0
  12. data/lib/ably/models/channel_status.rb +63 -0
  13. data/lib/ably/models/cipher_params.rb +137 -0
  14. data/lib/ably/models/connection_details.rb +149 -0
  15. data/lib/ably/models/connection_state_change.rb +83 -0
  16. data/lib/ably/models/delta_extras.rb +28 -0
  17. data/lib/ably/models/device_details.rb +126 -0
  18. data/lib/ably/models/device_push_details.rb +94 -0
  19. data/lib/ably/models/error_info.rb +104 -0
  20. data/lib/ably/models/http_paginated_response.rb +112 -0
  21. data/lib/ably/models/idiomatic_ruby_wrapper.rb +235 -0
  22. data/lib/ably/models/message.rb +223 -0
  23. data/lib/ably/models/message_encoders/base.rb +135 -0
  24. data/lib/ably/models/message_encoders/base64.rb +40 -0
  25. data/lib/ably/models/message_encoders/cipher.rb +84 -0
  26. data/lib/ably/models/message_encoders/json.rb +34 -0
  27. data/lib/ably/models/message_encoders/utf8.rb +26 -0
  28. data/lib/ably/models/nil_logger.rb +20 -0
  29. data/lib/ably/models/paginated_result.rb +201 -0
  30. data/lib/ably/models/presence_message.rb +244 -0
  31. data/lib/ably/models/protocol_message.rb +288 -0
  32. data/lib/ably/models/push_channel_subscription.rb +114 -0
  33. data/lib/ably/models/stats.rb +242 -0
  34. data/lib/ably/models/stats_types.rb +107 -0
  35. data/lib/ably/models/token_details.rb +148 -0
  36. data/lib/ably/models/token_request.rb +180 -0
  37. data/lib/ably/modules/ably.rb +26 -0
  38. data/lib/ably/modules/async_wrapper.rb +69 -0
  39. data/lib/ably/modules/channels_collection.rb +105 -0
  40. data/lib/ably/modules/conversions.rb +153 -0
  41. data/lib/ably/modules/encodeable.rb +111 -0
  42. data/lib/ably/modules/enum.rb +230 -0
  43. data/lib/ably/modules/event_emitter.rb +200 -0
  44. data/lib/ably/modules/event_machine_helpers.rb +26 -0
  45. data/lib/ably/modules/exception_codes.rb +128 -0
  46. data/lib/ably/modules/http_helpers.rb +41 -0
  47. data/lib/ably/modules/message_emitter.rb +78 -0
  48. data/lib/ably/modules/message_pack.rb +14 -0
  49. data/lib/ably/modules/model_common.rb +84 -0
  50. data/lib/ably/modules/safe_deferrable.rb +83 -0
  51. data/lib/ably/modules/safe_yield.rb +41 -0
  52. data/lib/ably/modules/state_emitter.rb +181 -0
  53. data/lib/ably/modules/state_machine.rb +59 -0
  54. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  55. data/lib/ably/modules/uses_state_machine.rb +111 -0
  56. data/lib/ably/realtime/auth.rb +283 -0
  57. data/lib/ably/realtime/channel/channel_manager.rb +272 -0
  58. data/lib/ably/realtime/channel/channel_properties.rb +40 -0
  59. data/lib/ably/realtime/channel/channel_state_machine.rb +104 -0
  60. data/lib/ably/realtime/channel/publisher.rb +82 -0
  61. data/lib/ably/realtime/channel/push_channel.rb +69 -0
  62. data/lib/ably/realtime/channel.rb +440 -0
  63. data/lib/ably/realtime/channels.rb +71 -0
  64. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +225 -0
  65. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +87 -0
  66. data/lib/ably/realtime/client.rb +363 -0
  67. data/lib/ably/realtime/connection/connection_manager.rb +594 -0
  68. data/lib/ably/realtime/connection/connection_state_machine.rb +144 -0
  69. data/lib/ably/realtime/connection/websocket_transport.rb +287 -0
  70. data/lib/ably/realtime/connection.rb +738 -0
  71. data/lib/ably/realtime/models/nil_channel.rb +30 -0
  72. data/lib/ably/realtime/presence/members_map.rb +395 -0
  73. data/lib/ably/realtime/presence/presence_manager.rb +78 -0
  74. data/lib/ably/realtime/presence/presence_state_machine.rb +57 -0
  75. data/lib/ably/realtime/presence.rb +502 -0
  76. data/lib/ably/realtime/push/admin.rb +75 -0
  77. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  78. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  79. data/lib/ably/realtime/push.rb +25 -0
  80. data/lib/ably/realtime/recovery_key_context.rb +36 -0
  81. data/lib/ably/realtime.rb +60 -0
  82. data/lib/ably/rest/channel/push_channel.rb +59 -0
  83. data/lib/ably/rest/channel.rb +194 -0
  84. data/lib/ably/rest/channels.rb +47 -0
  85. data/lib/ably/rest/client.rb +727 -0
  86. data/lib/ably/rest/middleware/encoder.rb +49 -0
  87. data/lib/ably/rest/middleware/exceptions.rb +53 -0
  88. data/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  89. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +22 -0
  90. data/lib/ably/rest/middleware/logger.rb +58 -0
  91. data/lib/ably/rest/middleware/parse_json.rb +27 -0
  92. data/lib/ably/rest/middleware/parse_message_pack.rb +43 -0
  93. data/lib/ably/rest/presence.rb +113 -0
  94. data/lib/ably/rest/push/admin.rb +58 -0
  95. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  96. data/lib/ably/rest/push/device_registrations.rb +114 -0
  97. data/lib/ably/rest/push.rb +25 -0
  98. data/lib/ably/rest.rb +40 -0
  99. data/lib/ably/util/ably_extensions.rb +29 -0
  100. data/lib/ably/util/crypto.rb +134 -0
  101. data/lib/ably/util/pub_sub.rb +45 -0
  102. data/lib/ably/util/safe_deferrable.rb +44 -0
  103. data/lib/ably/version.rb +7 -0
  104. data/lib/ably.rb +16 -0
  105. 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