ably 0.8.3 → 0.8.4

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 (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