ably-rest 0.7.3 → 0.7.5

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 (69) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +1 -0
  3. data/SPEC.md +480 -472
  4. data/lib/ably-rest.rb +1 -1
  5. data/lib/submodules/ably-ruby/LICENSE.txt +1 -1
  6. data/lib/submodules/ably-ruby/README.md +107 -24
  7. data/lib/submodules/ably-ruby/SPEC.md +531 -398
  8. data/lib/submodules/ably-ruby/lib/ably/auth.rb +24 -16
  9. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +9 -0
  10. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +17 -9
  11. data/lib/submodules/ably-ruby/lib/ably/models/paginated_resource.rb +12 -8
  12. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +18 -10
  13. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +15 -4
  14. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +4 -3
  15. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +31 -2
  16. data/lib/submodules/ably-ruby/lib/ably/modules/message_emitter.rb +77 -0
  17. data/lib/submodules/ably-ruby/lib/ably/modules/safe_deferrable.rb +71 -0
  18. data/lib/submodules/ably-ruby/lib/ably/modules/safe_yield.rb +41 -0
  19. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +28 -8
  20. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +0 -5
  21. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +24 -29
  22. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +54 -11
  23. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +21 -6
  24. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +7 -2
  25. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +29 -26
  26. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +4 -4
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +41 -9
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +72 -24
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +26 -4
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +19 -6
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +74 -208
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +264 -0
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_manager.rb +59 -0
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/presence_state_machine.rb +64 -0
  35. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +1 -1
  36. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +6 -2
  37. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -1
  38. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +3 -1
  39. data/lib/submodules/ably-ruby/lib/ably/util/safe_deferrable.rb +18 -0
  40. data/lib/submodules/ably-ruby/lib/ably/version.rb +1 -1
  41. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +2 -2
  42. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +28 -6
  43. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +116 -46
  44. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +55 -10
  45. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +32 -0
  46. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +456 -96
  47. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +2 -2
  48. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +2 -2
  49. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +96 -7
  50. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +8 -0
  51. data/lib/submodules/ably-ruby/spec/shared/safe_deferrable_behaviour.rb +71 -0
  52. data/lib/submodules/ably-ruby/spec/support/api_helper.rb +1 -1
  53. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +1 -1
  54. data/lib/submodules/ably-ruby/spec/support/test_app.rb +13 -7
  55. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +15 -14
  56. data/lib/submodules/ably-ruby/spec/unit/models/paginated_resource_spec.rb +4 -4
  57. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +17 -17
  58. data/lib/submodules/ably-ruby/spec/unit/models/stat_spec.rb +4 -4
  59. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +28 -9
  60. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +50 -0
  61. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +76 -2
  62. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +51 -20
  63. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +3 -3
  64. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +30 -0
  65. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +52 -26
  66. data/lib/submodules/ably-ruby/spec/unit/realtime/safe_deferrable_spec.rb +12 -0
  67. data/spec/spec_helper.rb +5 -0
  68. metadata +12 -4
  69. data/lib/submodules/ably-ruby/.ruby-version.old +0 -1
@@ -0,0 +1,264 @@
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,
24
+ :in_sync,
25
+ :failed
26
+ )
27
+ include Ably::Modules::StateEmitter
28
+
29
+ # Number of absent members to cache internally whilst channel is in sync.
30
+ # Cache is unlimited until initial sync is complete ensuring users who have left are never reported as present.
31
+ MAX_ABSENT_MEMBER_CACHE = 100
32
+
33
+ def initialize(presence)
34
+ @presence = presence
35
+
36
+ @state = STATE(:initialized)
37
+ @members = Hash.new
38
+ @absent_member_cleanup_queue = []
39
+
40
+ setup_event_handlers
41
+ end
42
+
43
+ # When attaching to a channel that has members present, the server
44
+ # initiates a sync automatically so that the client has a complete list of members.
45
+ #
46
+ # Until this sync is complete, this method returns false
47
+ #
48
+ # @return [Boolean]
49
+ def sync_complete?
50
+ in_sync?
51
+ end
52
+
53
+ # Update the SYNC serial from the ProtocolMessage so that SYNC can be resumed.
54
+ # If the serial is nil, or the part after the first : is empty, then the SYNC is complete
55
+ #
56
+ # @return [void]
57
+ #
58
+ # @api private
59
+ def update_sync_serial(serial)
60
+ @sync_serial = serial
61
+ change_state :in_sync if sync_serial_cursor_at_end?
62
+ end
63
+
64
+ # Get the list of presence members
65
+ #
66
+ # @param [Hash,String] options an options Hash to filter members
67
+ # @option options [String] :client_id optional client_id for the member
68
+ # @option options [String] :connection_id optional connection_id for the member
69
+ # @option options [String] :wait_for_sync defaults to true, if false the get method returns the current list of members and does not wait for the presence sync to complete
70
+ #
71
+ # @yield [Array<Ably::Models::PresenceMessage>] array of present members
72
+ #
73
+ # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
74
+ #
75
+ def get(options = {}, &block)
76
+ wait_for_sync = options.fetch(:wait_for_sync, true)
77
+ deferrable = Ably::Util::SafeDeferrable.new(logger)
78
+
79
+ result_block = proc do
80
+ present_members.tap do |members|
81
+ members.keep_if { |member| member.connection_id == options[:connection_id] } if options[:connection_id]
82
+ members.keep_if { |member| member.client_id == options[:client_id] } if options[:client_id]
83
+ end.tap do |members|
84
+ safe_yield block, members if block_given?
85
+ deferrable.succeed members
86
+ end
87
+ end
88
+
89
+ if !wait_for_sync || sync_complete?
90
+ result_block.call
91
+ else
92
+ # Must be defined before subsequent procs reference this callback
93
+ reset_callbacks = nil
94
+
95
+ in_sync_callback = proc do
96
+ reset_callbacks
97
+ result_block.call
98
+ end
99
+
100
+ failed_callback = proc do |error|
101
+ reset_callbacks
102
+ deferrable.fail error
103
+ end
104
+
105
+ reset_callbacks = proc do
106
+ off &in_sync_callback
107
+ off &failed_callback
108
+ channel.off &failed_callback
109
+ end
110
+
111
+ once :in_sync, &in_sync_callback
112
+
113
+ once(:failed, &failed_callback)
114
+ channel.unsafe_once(:detaching, :detached, :failed) do |error_reason|
115
+ failed_callback.call error_reason
116
+ end
117
+ end
118
+
119
+ deferrable
120
+ end
121
+
122
+ # @!attribute [r] length
123
+ # @return [Integer] number of present members known at this point in time, will not wait for sync operation to complete
124
+ def length
125
+ present_members.length
126
+ end
127
+ alias_method :count, :length
128
+ alias_method :size, :length
129
+
130
+ # Method to allow {MembersMap} to be {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
131
+ # @note this method will not wait for the sync operation to complete so may return an incomplete set of members. Use {MembersMap#get} instead.
132
+ def each(&block)
133
+ return to_enum(:each) unless block_given?
134
+ present_members.each(&block)
135
+ end
136
+
137
+ private
138
+ attr_reader :members, :sync_serial, :presence, :absent_member_cleanup_queue
139
+
140
+ def channel
141
+ presence.channel
142
+ end
143
+
144
+ def client
145
+ channel.client
146
+ end
147
+
148
+ def logger
149
+ client.logger
150
+ end
151
+
152
+ def connection
153
+ client.connection
154
+ end
155
+
156
+ def setup_event_handlers
157
+ presence.__incoming_msgbus__.subscribe(:presence, :sync) do |presence_message|
158
+ presence_message.decode channel
159
+ update_members_and_emit_events presence_message
160
+ end
161
+
162
+ resume_sync_proc = method(:resume_sync).to_proc
163
+ connection.on_resume &resume_sync_proc
164
+ once(:in_sync, :failed) do
165
+ connection.off_resume &resume_sync_proc
166
+ end
167
+ end
168
+
169
+ # Trigger a manual SYNC operation to resume member synchronisation from last known cursor position
170
+ def resume_sync
171
+ connection.send_protocol_message(
172
+ action: Ably::Models::ProtocolMessage::ACTION.Sync.to_i,
173
+ channel: channel.name,
174
+ channel_serial: sync_serial
175
+ )
176
+ end
177
+
178
+ # When channel serial in ProtocolMessage SYNC is nil or
179
+ # an empty cursor appears after the ':' such as 'cf30e75054887:psl_7g:client:189'.
180
+ # That is an indication that there are no more SYNC messages.
181
+ def sync_serial_cursor_at_end?
182
+ sync_serial.nil? || sync_serial.to_s.match(/^[\w-]+:?$/)
183
+ end
184
+
185
+ def update_members_and_emit_events(presence_message)
186
+ return unless ensure_presence_message_is_valid(presence_message)
187
+
188
+ unless should_update_member?(presence_message)
189
+ logger.debug "#{self.class.name}: Skipped presence member #{presence_message.action} on channel #{presence.channel.name}.\n#{presence_message.to_json}"
190
+ return
191
+ end
192
+
193
+ case presence_message.action
194
+ when Ably::Models::PresenceMessage::ACTION.Enter, Ably::Models::PresenceMessage::ACTION.Update, Ably::Models::PresenceMessage::ACTION.Present
195
+ add_presence_member presence_message
196
+ when Ably::Models::PresenceMessage::ACTION.Leave
197
+ remove_presence_member presence_message
198
+ else
199
+ Ably::Exceptions::ProtocolError.new("Protocol error, unknown presence action #{presence_message.action}", 400, 80013)
200
+ end
201
+
202
+ clean_up_absent_members
203
+ end
204
+
205
+ def ensure_presence_message_is_valid(presence_message)
206
+ return true if presence_message.connection_id
207
+
208
+ error = Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing connectionId", 400, 80013)
209
+ logger.error "PresenceMap: On channel '#{channel.name}' error: #{error}"
210
+ channel.trigger :error, error
211
+ end
212
+
213
+ # If the message received is older than the last known event for presence
214
+ # then skip. This can occur during a SYNC operation. For example:
215
+ # - SYNC starts
216
+ # - LEAVE event received for clientId 5
217
+ # - SYNC present even received for clientId 5 with a timestamp before LEAVE event because the LEAVE occured before the SYNC operation completed
218
+ #
219
+ # @return [Boolean]
220
+ #
221
+ def should_update_member?(presence_message)
222
+ if members[presence_message.member_key]
223
+ members[presence_message.member_key].fetch(:message).timestamp < presence_message.timestamp
224
+ else
225
+ true
226
+ end
227
+ end
228
+
229
+ def add_presence_member(presence_message)
230
+ 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}"
231
+ members[presence_message.member_key] = { present: true, message: presence_message }
232
+ presence.emit_message presence_message.action, presence_message
233
+ end
234
+
235
+ def remove_presence_member(presence_message)
236
+ logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_json}"
237
+ members[presence_message.member_key] = { present: false, message: presence_message }
238
+ absent_member_cleanup_queue << presence_message.member_key
239
+ presence.emit_message presence_message.action, presence_message
240
+ end
241
+
242
+ def present_members
243
+ members.select do |key, presence|
244
+ presence.fetch(:present)
245
+ end.map do |key, presence|
246
+ presence.fetch(:message)
247
+ end
248
+ end
249
+
250
+ def absent_members
251
+ members.reject do |key, presence|
252
+ presence.fetch(:present)
253
+ end.map do |key, presence|
254
+ presence.fetch(:message)
255
+ end
256
+ end
257
+
258
+ def clean_up_absent_members
259
+ return unless sync_complete?
260
+ members.delete absent_member_cleanup_queue.shift until absent_member_cleanup_queue.count <= MAX_ABSENT_MEMBER_CACHE
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,59 @@
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
+ # Expect SYNC ProtocolMessages from the server with a list of current members on this channel
23
+ #
24
+ # @return [void]
25
+ #
26
+ # @api private
27
+ def sync_expected
28
+ presence.members.change_state :sync_starting
29
+ end
30
+
31
+ # There server has indicated that there are no SYNC ProtocolMessages to come because
32
+ # there are no members on this channel
33
+ #
34
+ # @return [void]
35
+ #
36
+ # @api private
37
+ def sync_not_expected
38
+ presence.members.change_state :in_sync
39
+ end
40
+
41
+ private
42
+ def_delegators :presence, :members, :channel
43
+
44
+ def setup_channel_event_handlers
45
+ channel.unsafe_on(:detached) do
46
+ presence.transition_state_machine :left if presence.can_transition_to?(:left)
47
+ end
48
+
49
+ channel.unsafe_on(:failed) do |metadata|
50
+ presence.transition_state_machine :failed, metadata if presence.can_transition_to?(:failed)
51
+ end
52
+
53
+ presence.unsafe_on(:entered) do |message|
54
+ presence.set_connection_id message.connection_id
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,64 @@
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
+ # :failed
19
+ Presence::STATE.each_with_index do |state_enum, index|
20
+ state state_enum.to_sym, initial: index == 0
21
+ end
22
+
23
+ # Entering or entered states can skip leaving and go straight to left if a channel is detached
24
+ # A channel that detaches very quickly will also go straight to :left from :initialized
25
+ # Failed states only occur when present and the channel fails or presence fails
26
+ transition :from => :initialized, :to => [:entering, :left]
27
+ transition :from => :entering, :to => [:entered, :leaving, :left, :failed]
28
+ transition :from => :entered, :to => [:leaving, :left, :failed]
29
+ transition :from => :leaving, :to => [:left, :entering, :failed]
30
+ transition :from => :failed, :to => [:entering]
31
+
32
+ after_transition do |presence, transition|
33
+ presence.synchronize_state_with_statemachine
34
+ end
35
+
36
+ after_transition(to: [:entering]) do |presence, current_transition|
37
+ presence.manager.enter current_transition.metadata
38
+ end
39
+
40
+ after_transition(to: [:leaving]) do |presence, current_transition|
41
+ presence.manager.leave current_transition.metadata
42
+ end
43
+
44
+ after_transition(to: [:failed]) do |presence, current_transition|
45
+ presence.manager.emit_error current_transition.metadata
46
+ end
47
+
48
+ # Transitions responsible for updating channel#error_reason
49
+ before_transition(to: [:left, :failed]) do |presence, current_transition|
50
+ presence.channel.set_failed_channel_error_reason current_transition.metadata if is_error_type?(current_transition.metadata)
51
+ end
52
+
53
+ private
54
+ def channel
55
+ object.channel
56
+ end
57
+
58
+ # Logged needs to be defined as it is used by {Ably::Modules::StateMachine}
59
+ def logger
60
+ channel.logger
61
+ end
62
+ end
63
+ end
64
+ end
@@ -43,7 +43,7 @@ module Ably
43
43
  data: data
44
44
  }
45
45
 
46
- message = Ably::Models::Message.new(payload, nil).tap do |message|
46
+ message = Ably::Models::Message.new(payload).tap do |message|
47
47
  message.encode self
48
48
  end
49
49
 
@@ -67,7 +67,7 @@ module Ably
67
67
 
68
68
  # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
69
69
  #
70
- # @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key
70
+ # @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key or Token ID
71
71
  # @option options (see Ably::Auth#authorise)
72
72
  # @option options [Boolean] :tls TLS is used by default, providing a value of false disables TLS. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
73
73
  # @option options [String] :api_key API key comprising the key ID and key secret in a single string
@@ -96,7 +96,11 @@ module Ably
96
96
 
97
97
  options = options.clone
98
98
  if options.kind_of?(String)
99
- options = { api_key: options }
99
+ options = if options.match(/^[\w]{2,}\.[\w]{2,}:[\w]{2,}$/)
100
+ { api_key: options }
101
+ else
102
+ { token_id: options }
103
+ end
100
104
  end
101
105
 
102
106
  @tls = options.delete(:tls) == false ? false : true
@@ -83,7 +83,7 @@ module Ably
83
83
  end
84
84
 
85
85
  def decode_message(presence_message)
86
- presence_message.decode self.channel
86
+ presence_message.decode channel
87
87
  rescue Ably::Exceptions::CipherError, Ably::Exceptions::EncoderError => e
88
88
  client.logger.error "Decoding Error on presence channel '#{channel.name}', presence message client_id '#{presence_message.client_id}'. #{e.class.name}: #{e.message}"
89
89
  end
@@ -1,3 +1,5 @@
1
+ require 'ably/modules/event_emitter.rb'
2
+
1
3
  module Ably::Util
2
4
  # PubSub class provides methods to publish & subscribe to events, with methods and naming
3
5
  # intentionally different to EventEmitter as it is intended for private message handling
@@ -34,7 +36,7 @@ module Ably::Util
34
36
  self.class.instance_eval do
35
37
  configure_event_emitter options
36
38
 
37
- alias_method :subscribe, :on
39
+ alias_method :subscribe, :unsafe_on
38
40
  alias_method :publish, :trigger
39
41
  alias_method :unsubscribe, :off
40
42
  end
@@ -0,0 +1,18 @@
1
+ module Ably::Util
2
+ # SafeDeferrable class provides a Deferrable that is safe to use for for public interfaces
3
+ # of this client library. Any exceptions raised in the success or failure callbacks is
4
+ # caught and logged to the provided logger.
5
+ #
6
+ # An exception in a callback provided by a developer should not break this client library
7
+ # and stop further execution of code.
8
+ #
9
+ class SafeDeferrable
10
+ include Ably::Modules::SafeDeferrable
11
+
12
+ attr_reader :logger
13
+
14
+ def initialize(logger)
15
+ @logger = logger
16
+ end
17
+ end
18
+ end