ably 0.8.3 → 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -3
  3. data/lib/ably/auth.rb +1 -1
  4. data/lib/ably/exceptions.rb +3 -0
  5. data/lib/ably/models/channel_state_change.rb +41 -0
  6. data/lib/ably/models/connection_state_change.rb +43 -0
  7. data/lib/ably/models/message.rb +1 -1
  8. data/lib/ably/models/presence_message.rb +1 -1
  9. data/lib/ably/models/protocol_message.rb +2 -1
  10. data/lib/ably/modules/state_emitter.rb +4 -1
  11. data/lib/ably/modules/uses_state_machine.rb +28 -4
  12. data/lib/ably/realtime/channel.rb +11 -3
  13. data/lib/ably/realtime/channel/channel_manager.rb +24 -4
  14. data/lib/ably/realtime/channel/channel_state_machine.rb +20 -11
  15. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +4 -4
  16. data/lib/ably/realtime/connection.rb +1 -0
  17. data/lib/ably/realtime/connection/connection_manager.rb +33 -21
  18. data/lib/ably/realtime/connection/connection_state_machine.rb +24 -16
  19. data/lib/ably/rest/channel.rb +3 -2
  20. data/lib/ably/util/crypto.rb +15 -0
  21. data/lib/ably/version.rb +1 -1
  22. data/spec/acceptance/realtime/channel_spec.rb +155 -9
  23. data/spec/acceptance/realtime/client_spec.rb +2 -2
  24. data/spec/acceptance/realtime/connection_failures_spec.rb +8 -4
  25. data/spec/acceptance/realtime/connection_spec.rb +122 -11
  26. data/spec/acceptance/realtime/message_spec.rb +119 -3
  27. data/spec/acceptance/realtime/presence_spec.rb +34 -13
  28. data/spec/acceptance/rest/channel_spec.rb +9 -0
  29. data/spec/acceptance/rest/client_spec.rb +10 -0
  30. data/spec/unit/models/channel_state_change_spec.rb +44 -0
  31. data/spec/unit/models/connection_state_change_spec.rb +54 -0
  32. data/spec/unit/util/crypto_spec.rb +18 -0
  33. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4a4a37e1a1ac2bddad5dadd6b97b77dbc93acc76
4
- data.tar.gz: 69ebb5298665efbf1ad4d9255b76cd8c581b27e5
3
+ metadata.gz: 85089829c198d66df277d8b4c675497c3632ed88
4
+ data.tar.gz: c6a05542c8c35adce99763b651290cb3e5fbf472
5
5
  SHA512:
6
- metadata.gz: a0566f39324a88f7d00efea0292fd04af77b423581df8e920127a425b22328464035fc36f678fdebe40c2b4fb68b8f653e86c7b1a29ad14a0086f138acc582cb
7
- data.tar.gz: ef77958e46559356a6db5929ec630c69ab1d398916e20d82c10a2836ef73d834fcc5020a8809a3f56f2efa44132bd58b77cdf385e804f51187ced597962be016
6
+ metadata.gz: b13264f6a68b29dcb4ccd0f7bc87af4df57cfb9d685bd7f87ebda5bef3c7679a437dd2ee5f967e0eb2385ab3659588784b32ae6f39520931723e1407d9471b2a
7
+ data.tar.gz: 5d5f15f00cce1ddced2188d2e3c5a8c3f1b36a719d281b54738eded944db6127e5546df65048118935d35ee6ea8b588ceab2cadb4f8ac66d0998a609e5aaba54
@@ -1,11 +1,17 @@
1
1
  # Change Log
2
2
 
3
- ## [v0.8.3](https://github.com/ably/ably-ruby/tree/v0.8.3)
3
+ ## [Unreleased](https://github.com/ably/ably-ruby/tree/HEAD)
4
4
 
5
- [Full Changelog](https://github.com/ably/ably-ruby/compare/v0.8.2...v0.8.3)
5
+ [Full Changelog](https://github.com/ably/ably-ruby/compare/v0.8.2...HEAD)
6
6
 
7
7
  **Implemented enhancements:**
8
8
 
9
+ - Add compatibility support for default Crypto params [\#53](https://github.com/ably/ably-ruby/issues/53)
10
+
11
+ - EventEmitter on connection [\#52](https://github.com/ably/ably-ruby/issues/52)
12
+
13
+ - Add test for connectionId attribute for a message sent over REST [\#50](https://github.com/ably/ably-ruby/issues/50)
14
+
9
15
  - Implement :queue\_messages option [\#36](https://github.com/ably/ably-ruby/issues/36)
10
16
 
11
17
  - Check that a non 200-299 status code for REST requests uses fallback hosts [\#35](https://github.com/ably/ably-ruby/issues/35)
@@ -32,6 +38,10 @@
32
38
 
33
39
  **Merged pull requests:**
34
40
 
41
+ - Spec update to fix a number of issues [\#60](https://github.com/ably/ably-ruby/pull/60) ([mattheworiordan](https://github.com/mattheworiordan))
42
+
43
+ - Allow clientId to be provided on init if using externally created token [\#58](https://github.com/ably/ably-ruby/pull/58) ([SimonWoolf](https://github.com/SimonWoolf))
44
+
35
45
  - Separate token params for auth [\#57](https://github.com/ably/ably-ruby/pull/57) ([mattheworiordan](https://github.com/mattheworiordan))
36
46
 
37
47
  - Ensure files are required in a consistent order [\#51](https://github.com/ably/ably-ruby/pull/51) ([SimonWoolf](https://github.com/SimonWoolf))
@@ -182,4 +192,4 @@
182
192
 
183
193
 
184
194
 
185
- \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
195
+ \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
@@ -74,7 +74,7 @@ module Ably
74
74
  raise ArgumentError, 'key is missing. Either an API key, token, or token auth method must be provided'
75
75
  end
76
76
 
77
- if has_client_id?
77
+ if has_client_id? && !token_creatable_externally?
78
78
  raise ArgumentError, 'client_id cannot be provided without a complete API key. Key name & Secret is needed to authenticate with Ably and obtain a token' unless api_key_present?
79
79
  ensure_utf_8 :client_id, client_id
80
80
  end
@@ -58,6 +58,9 @@ module Ably
58
58
  # Connection Timeout accessing Realtime or REST service
59
59
  class ConnectionTimeout < ConnectionError; end
60
60
 
61
+ # Transport closed unexpectedly
62
+ class TransportClosed < ConnectionError; end
63
+
61
64
  # Connection closed unexpectedly
62
65
  class ConnectionClosed < ConnectionError; end
63
66
 
@@ -0,0 +1,41 @@
1
+ module Ably::Models
2
+ # ChannelStateChange is a class that is emitted by the {Ably::Realtime::Channel} object
3
+ # when a state change occurs
4
+ #
5
+ # @!attribute [r] current
6
+ # @return [Connection::STATE] Current connection state
7
+ # @!attribute [r] previous
8
+ # @return [Connection::STATE] Previous connection state
9
+ # @!attribute [r] reason
10
+ # @return [Ably::Models::ErrorInfo] Object describing the reason for a state change when not initiated by the consumer of the client library
11
+ #
12
+ class ChannelStateChange
13
+ include Ably::Modules::ModelCommon
14
+
15
+ def initialize(hash_object)
16
+ unless (hash_object.keys - [:current, :previous, :reason, :protocol_message]).empty?
17
+ raise ArgumentError, 'Invalid attributes, expecting :current, :previous, :reason'
18
+ end
19
+
20
+ @hash_object = {
21
+ current: hash_object.fetch(:current),
22
+ previous: hash_object.fetch(:previous),
23
+ retry_in: hash_object[:retry_in],
24
+ reason: hash_object[:reason],
25
+ protocol_message: hash_object[:protocol_message]
26
+ }
27
+ rescue KeyError => e
28
+ raise ArgumentError, e
29
+ end
30
+
31
+ %w(current previous reason protocol_message).each do |attribute|
32
+ define_method attribute do
33
+ @hash_object[attribute.to_sym]
34
+ end
35
+ end
36
+
37
+ def to_s
38
+ "ChannelStateChange: current state #{current}, previous state #{previous}"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ module Ably::Models
2
+ # ConnectionStateChange is a class that is emitted by the {Ably::Realtime::Connection} object
3
+ # when a state change occurs
4
+ #
5
+ # @!attribute [r] current
6
+ # @return [Connection::STATE] Current connection state
7
+ # @!attribute [r] previous
8
+ # @return [Connection::STATE] Previous connection state
9
+ # @!attribute [r] retry_in
10
+ # @return [Integer] Time in seconds until the connection will reattempt to connect when in the +:disconnected+ or +:suspended+ state
11
+ # @!attribute [r] reason
12
+ # @return [Ably::Models::ErrorInfo] Object describing the reason for a state change when not initiated by the consumer of the client library
13
+ #
14
+ class ConnectionStateChange
15
+ include Ably::Modules::ModelCommon
16
+
17
+ def initialize(hash_object)
18
+ unless (hash_object.keys - [:current, :previous, :retry_in, :reason, :protocol_message]).empty?
19
+ raise ArgumentError, 'Invalid attributes, expecting :current, :previous, :retry_in, :reason'
20
+ end
21
+
22
+ @hash_object = {
23
+ current: hash_object.fetch(:current),
24
+ previous: hash_object.fetch(:previous),
25
+ retry_in: hash_object[:retry_in],
26
+ reason: hash_object[:reason],
27
+ protocol_message: hash_object[:protocol_message]
28
+ }
29
+ rescue KeyError => e
30
+ raise ArgumentError, e
31
+ end
32
+
33
+ %w(current previous retry_in reason protocol_message).each do |attribute|
34
+ define_method attribute do
35
+ @hash_object[attribute.to_sym]
36
+ end
37
+ end
38
+
39
+ def to_s
40
+ "ConnectionStateChange: current state #{current}, previous state #{previous}"
41
+ end
42
+ end
43
+ end
@@ -131,7 +131,7 @@ module Ably::Models
131
131
  end
132
132
 
133
133
  def logger
134
- return logger if logger
134
+ return @logger if @logger
135
135
  protocol_message.logger if protocol_message
136
136
  end
137
137
  end
@@ -154,7 +154,7 @@ module Ably::Models
154
154
  end
155
155
 
156
156
  def logger
157
- return logger if logger
157
+ return @logger if @logger
158
158
  protocol_message.logger if protocol_message
159
159
  end
160
160
  end
@@ -226,7 +226,8 @@ module Ably::Models
226
226
  !action_enum || (ack_required? && !has_serial?)
227
227
  end
228
228
 
229
- private
229
+ # @!attribute [r] logger
230
+ # @api private
230
231
  attr_reader :logger
231
232
  end
232
233
  end
@@ -141,7 +141,10 @@ module Ably::Modules
141
141
  #
142
142
  def deferrable_for_state_change_to(target_state)
143
143
  Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
144
- once_or_if(target_state, else: proc { |*args| deferrable.fail self, *args }) do
144
+ fail_proc = Proc.new do |state_change|
145
+ deferrable.fail self, state_change.reason
146
+ end
147
+ once_or_if(target_state, else: fail_proc) do
145
148
  yield self if block_given?
146
149
  deferrable.succeed self
147
150
  end
@@ -13,8 +13,8 @@ module Ably::Modules
13
13
  #
14
14
  # @return [Boolean] true if new_state can be transitioned to by state machine
15
15
  # @api private
16
- def transition_state_machine(new_state, emit_object = nil)
17
- state_machine.transition_state(new_state, emit_object)
16
+ def transition_state_machine(new_state, emit_params = {})
17
+ state_machine.transition_state(new_state, emit_object(new_state, emit_params))
18
18
  end
19
19
 
20
20
  # Call #transition_to! on the StateMachine
@@ -22,8 +22,8 @@ module Ably::Modules
22
22
  #
23
23
  # @return [void]
24
24
  # @api private
25
- def transition_state_machine!(new_state, emit_object = nil)
26
- state_machine.transition_to!(new_state, emit_object)
25
+ def transition_state_machine!(new_state, emit_params = {})
26
+ state_machine.transition_to!(new_state, emit_object(new_state, emit_params))
27
27
  end
28
28
 
29
29
  # Provides an internal method for this object's state to match the StateMachine's current state.
@@ -70,5 +70,29 @@ module Ably::Modules
70
70
  logger.debug "#{self.class.name}: Transitioned to #{state_machine.current_state}"
71
71
  end
72
72
  end
73
+
74
+ def emit_object(new_state, emit_params)
75
+ if self.class.emits_klass
76
+ self.class.emits_klass.new((emit_params || {}).merge(current: STATE(new_state), previous: STATE(state_machine.current_state)))
77
+ else
78
+ emit_params
79
+ end
80
+ end
81
+
82
+ def self.included(base)
83
+ base.extend(ClassMethods)
84
+ end
85
+
86
+ module ClassMethods
87
+ def emits_klass
88
+ @emits_klass ||= if @emits_klass_name
89
+ Object.const_get @emits_klass_name
90
+ end
91
+ end
92
+
93
+ def ensure_state_machine_emits(klass)
94
+ @emits_klass_name = klass
95
+ end
96
+ end
73
97
  end
74
98
  end
@@ -46,6 +46,7 @@ module Ably
46
46
  )
47
47
  include Ably::Modules::StateEmitter
48
48
  include Ably::Modules::UsesStateMachine
49
+ ensure_state_machine_emits 'Ably::Models::ChannelStateChange'
49
50
 
50
51
  # Max number of messages to bundle in a single ProtocolMessage
51
52
  MAX_PROTOCOL_MESSAGE_BATCH_SIZE = 50
@@ -106,6 +107,7 @@ module Ably
106
107
  #
107
108
  # @param name [String, Array<Ably::Models::Message|Hash>, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs
108
109
  # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
110
+ # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string
109
111
  #
110
112
  # @yield [Ably::Models::Message,Array<Ably::Models::Message>] On success, will call the block with the {Ably::Models::Message} if a single message is publishde, or an Array of {Ably::Models::Message} when multiple messages are published
111
113
  # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
@@ -136,7 +138,7 @@ module Ably
136
138
  # puts "#{message.name} was not received, error #{error.message}"
137
139
  # end
138
140
  #
139
- def publish(name, data = nil, &success_block)
141
+ def publish(name, data = nil, attributes = {}, &success_block)
140
142
  raise Ably::Exceptions::ChannelInactive.new('Cannot publish messages on a detached channel') if detached? || detaching?
141
143
  raise Ably::Exceptions::ChannelInactive.new('Cannot publish messages on a failed channel') if failed?
142
144
 
@@ -149,7 +151,7 @@ module Ably
149
151
  else
150
152
  ensure_utf_8 :name, name, allow_nil: true
151
153
  ensure_supported_payload data
152
- [{ name: name, data: data }]
154
+ [{ name: name, data: data }.merge(attributes)]
153
155
  end
154
156
 
155
157
  queue_messages(messages).tap do |deferrable|
@@ -190,7 +192,9 @@ module Ably
190
192
  # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callback
191
193
  #
192
194
  def attach(&success_block)
193
- raise Ably::Exceptions::InvalidStateChange.new("Cannot ATTACH channel when the connection is in a closed, suspended or failed state. Connection state: #{connection.state}") if connection.closing? || connection.closed? || connection.suspended? || connection.failed?
195
+ if connection.closing? || connection.closed? || connection.suspended? || connection.failed?
196
+ raise Ably::Exceptions::InvalidStateChange.new("Cannot ATTACH channel when the connection is in a closed, suspended or failed state. Connection state: #{connection.state}")
197
+ end
194
198
 
195
199
  transition_state_machine :attaching if can_transition_to?(:attaching)
196
200
  deferrable_for_state_change_to(STATE.Attached, &success_block)
@@ -285,6 +289,10 @@ module Ably
285
289
  client.logger
286
290
  end
287
291
 
292
+ # As we are using a state machine, do not allow change_state to be used
293
+ # #transition_state_machine must be used instead
294
+ private :change_state
295
+
288
296
  private
289
297
  attr_reader :queue
290
298
 
@@ -27,7 +27,7 @@ module Ably::Realtime
27
27
  # Commence attachment
28
28
  def detach(error = nil)
29
29
  if connection.closed? || connection.connecting? || connection.suspended?
30
- channel.transition_state_machine :detached, error
30
+ channel.transition_state_machine :detached, reason: error
31
31
  elsif can_transition_to?(:detached)
32
32
  send_detach_protocol_message
33
33
  end
@@ -51,7 +51,7 @@ module Ably::Realtime
51
51
 
52
52
  # Detach a channel as a result of an error
53
53
  def suspend(error)
54
- channel.transition_state_machine! :detaching, error
54
+ channel.transition_state_machine! :detaching, reason: error
55
55
  end
56
56
 
57
57
  # When a channel is no longer attached or has failed,
@@ -118,6 +118,22 @@ module Ably::Realtime
118
118
  )
119
119
  end
120
120
 
121
+ # Any message sent before an ACK/NACK was received on the previous transport
122
+ # needs to be resent to the Ably service so that a subsequent ACK/NACK is received.
123
+ # It is up to Ably to ensure that duplicate messages are not retransmitted on the channel
124
+ # base on the serial numbers
125
+ #
126
+ # @api private
127
+ def resend_pending_message_ack_queue
128
+ connection.__pending_message_ack_queue__.delete_if do |protocol_message|
129
+ if protocol_message.channel == channel.name
130
+ connection.__outgoing_message_queue__ << protocol_message
131
+ connection.__outgoing_protocol_msgbus__.publish :protocol_message
132
+ true
133
+ end
134
+ end
135
+ end
136
+
121
137
  def setup_connection_event_handlers
122
138
  connection.unsafe_on(:closed) do
123
139
  channel.transition_state_machine :detaching if can_transition_to?(:detaching)
@@ -125,15 +141,19 @@ module Ably::Realtime
125
141
 
126
142
  connection.unsafe_on(:suspended) do |error|
127
143
  if can_transition_to?(:detaching)
128
- channel.transition_state_machine :detaching, Ably::Exceptions::ConnectionSuspended.new('Connection suspended', nil, 80002, error)
144
+ channel.transition_state_machine :detaching, reason: Ably::Exceptions::ConnectionSuspended.new('Connection suspended', nil, 80002, error)
129
145
  end
130
146
  end
131
147
 
132
148
  connection.unsafe_on(:failed) do |error|
133
149
  if can_transition_to?(:failed)
134
- channel.transition_state_machine :failed, Ably::Exceptions::ConnectionFailed.new('Connection failed', nil, 80002, error)
150
+ channel.transition_state_machine :failed, reason: Ably::Exceptions::ConnectionFailed.new('Connection failed', nil, 80002, error)
135
151
  end
136
152
  end
153
+
154
+ connection.unsafe_on(:connected) do |error|
155
+ resend_pending_message_ack_queue
156
+ end
137
157
  end
138
158
 
139
159
  def logger
@@ -35,39 +35,48 @@ module Ably::Realtime
35
35
  end
36
36
 
37
37
  before_transition(to: [:attached]) do |channel, current_transition|
38
- channel.manager.attached current_transition.metadata
38
+ channel.manager.attached current_transition.metadata.protocol_message
39
39
  end
40
40
 
41
41
  after_transition(to: [:detaching]) do |channel, current_transition|
42
- channel.manager.detach current_transition.metadata
42
+ err = error_from_state_change(current_transition)
43
+ channel.manager.detach err
43
44
  end
44
45
 
45
46
  after_transition(to: [:detached]) do |channel, current_transition|
46
- channel.manager.fail_messages_awaiting_ack nil_unless_error(current_transition.metadata)
47
- channel.manager.emit_error current_transition.metadata if is_error_type?(current_transition.metadata)
47
+ err = error_from_state_change(current_transition)
48
+ channel.manager.fail_messages_awaiting_ack err
49
+ channel.manager.emit_error err if err
48
50
  end
49
51
 
50
52
  after_transition(to: [:failed]) do |channel, current_transition|
51
- channel.manager.fail_messages_awaiting_ack nil_unless_error(current_transition.metadata)
52
- channel.manager.emit_error current_transition.metadata if is_error_type?(current_transition.metadata)
53
+ err = error_from_state_change(current_transition)
54
+ channel.manager.fail_messages_awaiting_ack err
55
+ channel.manager.emit_error err if err
53
56
  end
54
57
 
55
58
  # Transitions responsible for updating channel#error_reason
56
59
  before_transition(to: [:failed]) do |channel, current_transition|
57
- channel.set_failed_channel_error_reason current_transition.metadata if is_error_type?(current_transition.metadata)
60
+ err = error_from_state_change(current_transition)
61
+ channel.set_failed_channel_error_reason err if err
58
62
  end
59
63
 
60
64
  before_transition(to: [:attached, :detached]) do |channel, current_transition|
61
- if is_error_type?(current_transition.metadata)
62
- channel.set_failed_channel_error_reason current_transition.metadata
65
+ err = error_from_state_change(current_transition)
66
+ if err
67
+ channel.set_failed_channel_error_reason err
63
68
  else
64
69
  # Attached & Detached are "healthy" final states so reset the error reason
65
70
  channel.clear_error_reason
66
71
  end
67
72
  end
68
73
 
69
- def self.nil_unless_error(error_object)
70
- error_object if is_error_type?(error_object)
74
+ def self.error_from_state_change(current_transition)
75
+ # ChannelStateChange object is always passed in current_transition metadata object
76
+ connection_state_change = current_transition.metadata
77
+ # Reason attribute contains errors
78
+ err = connection_state_change && connection_state_change.reason
79
+ err if is_error_type?(err)
71
80
  end
72
81
 
73
82
  private
@@ -68,10 +68,10 @@ module Ably::Realtime
68
68
 
69
69
  when ACTION.Connect
70
70
  when ACTION.Connected
71
- connection.transition_state_machine :connected, protocol_message unless connection.connected?
71
+ connection.transition_state_machine :connected, reason: protocol_message.error, protocol_message: protocol_message unless connection.connected?
72
72
 
73
73
  when ACTION.Disconnect, ACTION.Disconnected
74
- connection.transition_state_machine :disconnected, protocol_message.error unless connection.disconnected?
74
+ connection.transition_state_machine :disconnected, reason: protocol_message.error unless connection.disconnected?
75
75
 
76
76
  when ACTION.Close
77
77
  when ACTION.Closed
@@ -87,7 +87,7 @@ module Ably::Realtime
87
87
  when ACTION.Attach
88
88
  when ACTION.Attached
89
89
  get_channel(protocol_message.channel).tap do |channel|
90
- channel.transition_state_machine :attached, protocol_message unless channel.attached?
90
+ channel.transition_state_machine :attached, reason: protocol_message.error, protocol_message: protocol_message unless channel.attached?
91
91
  end
92
92
 
93
93
  when ACTION.Detach
@@ -125,7 +125,7 @@ module Ably::Realtime
125
125
  def dispatch_channel_error(protocol_message)
126
126
  logger.warn "Channel Error message received: #{protocol_message.error}"
127
127
  if !protocol_message.has_message_serial?
128
- get_channel(protocol_message.channel).transition_state_machine :failed, protocol_message.error
128
+ get_channel(protocol_message.channel).transition_state_machine :failed, reason: protocol_message.error
129
129
  else
130
130
  logger.fatal "Cannot process ProtocolMessage as not yet implemented: #{protocol_message}"
131
131
  end