ably 1.1.4 → 1.1.8

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +15 -1
  3. data/CHANGELOG.md +109 -0
  4. data/COPYRIGHT +1 -1
  5. data/README.md +23 -9
  6. data/SPEC.md +289 -228
  7. data/ably.gemspec +14 -9
  8. data/lib/ably/agent.rb +3 -0
  9. data/lib/ably/exceptions.rb +6 -0
  10. data/lib/ably/models/connection_details.rb +8 -0
  11. data/lib/ably/models/delta_extras.rb +29 -0
  12. data/lib/ably/models/error_info.rb +6 -2
  13. data/lib/ably/models/message.rb +25 -0
  14. data/lib/ably/models/presence_message.rb +14 -0
  15. data/lib/ably/models/protocol_message.rb +13 -8
  16. data/lib/ably/modules/ably.rb +11 -1
  17. data/lib/ably/realtime/channel/channel_manager.rb +2 -2
  18. data/lib/ably/realtime/channel/channel_state_machine.rb +5 -1
  19. data/lib/ably/realtime/channel/publisher.rb +6 -0
  20. data/lib/ably/realtime/channel.rb +2 -0
  21. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -6
  22. data/lib/ably/realtime/client.rb +1 -0
  23. data/lib/ably/realtime/connection/connection_manager.rb +13 -4
  24. data/lib/ably/realtime/connection/connection_state_machine.rb +4 -0
  25. data/lib/ably/realtime/connection.rb +2 -2
  26. data/lib/ably/rest/channel.rb +11 -3
  27. data/lib/ably/rest/client.rb +37 -18
  28. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -1
  29. data/lib/ably/version.rb +1 -13
  30. data/lib/ably.rb +1 -0
  31. data/spec/acceptance/realtime/auth_spec.rb +1 -1
  32. data/spec/acceptance/realtime/channel_history_spec.rb +25 -0
  33. data/spec/acceptance/realtime/channel_spec.rb +220 -1
  34. data/spec/acceptance/realtime/connection_failures_spec.rb +85 -13
  35. data/spec/acceptance/realtime/connection_spec.rb +263 -32
  36. data/spec/acceptance/realtime/presence_history_spec.rb +3 -1
  37. data/spec/acceptance/realtime/presence_spec.rb +31 -159
  38. data/spec/acceptance/rest/base_spec.rb +8 -4
  39. data/spec/acceptance/rest/channel_spec.rb +84 -9
  40. data/spec/acceptance/rest/channels_spec.rb +1 -1
  41. data/spec/acceptance/rest/client_spec.rb +72 -33
  42. data/spec/shared/client_initializer_behaviour.rb +131 -0
  43. data/spec/shared/model_behaviour.rb +1 -1
  44. data/spec/spec_helper.rb +11 -2
  45. data/spec/support/test_app.rb +1 -1
  46. data/spec/unit/models/delta_extras_spec.rb +14 -0
  47. data/spec/unit/models/error_info_spec.rb +17 -1
  48. data/spec/unit/models/message_spec.rb +83 -0
  49. data/spec/unit/models/presence_message_spec.rb +49 -0
  50. data/spec/unit/models/protocol_message_spec.rb +72 -20
  51. data/spec/unit/realtime/channel_spec.rb +3 -2
  52. data/spec/unit/realtime/channels_spec.rb +3 -3
  53. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +38 -0
  54. data/spec/unit/rest/channel_spec.rb +44 -1
  55. data/spec/unit/rest/client_spec.rb +47 -0
  56. metadata +48 -36
data/ably.gemspec CHANGED
@@ -20,9 +20,9 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_runtime_dependency 'eventmachine', '~> 1.2.6'
22
22
  spec.add_runtime_dependency 'em-http-request', '~> 1.1'
23
- spec.add_runtime_dependency 'statesman', '~> 7.4'
24
- spec.add_runtime_dependency 'faraday', '>= 0.12', '< 2.0.0'
25
- spec.add_runtime_dependency 'excon', '~> 0.55'
23
+ spec.add_runtime_dependency 'statesman', '~> 8.0'
24
+ spec.add_runtime_dependency 'faraday', '~> 1.0'
25
+ spec.add_runtime_dependency 'typhoeus', '~> 1.4'
26
26
 
27
27
  if RUBY_VERSION.match(/^1\./)
28
28
  spec.add_runtime_dependency 'json', '< 2.0'
@@ -33,9 +33,9 @@ Gem::Specification.new do |spec|
33
33
  spec.add_runtime_dependency 'msgpack', '>= 1.3.0'
34
34
  spec.add_runtime_dependency 'addressable', '>= 2.0.0'
35
35
 
36
- spec.add_development_dependency 'rake', '~> 11.3'
36
+ spec.add_development_dependency 'rake', '~> 13.0'
37
37
  spec.add_development_dependency 'redcarpet', '~> 3.3'
38
- spec.add_development_dependency 'rspec', '~> 3.3.0' # version lock, see config.around(:example, :event_machine) in event_machine_helper.rb
38
+ spec.add_development_dependency 'rspec', '~> 3.10.0'
39
39
  spec.add_development_dependency 'rspec-retry', '~> 0.6'
40
40
  spec.add_development_dependency 'yard', '~> 0.9'
41
41
  spec.add_development_dependency 'rspec-instafail', '~> 1.0'
@@ -47,11 +47,16 @@ Gem::Specification.new do |spec|
47
47
  spec.add_development_dependency 'parallel_tests', '~> 2.9.0'
48
48
  else
49
49
  spec.add_development_dependency 'webmock', '~> 3.11'
50
- spec.add_development_dependency 'coveralls'
51
- spec.add_development_dependency 'parallel_tests', '~> 2.22'
50
+ spec.add_development_dependency 'simplecov', '~> 0.21.2'
51
+ spec.add_development_dependency 'simplecov-lcov', '~> 0.8.0'
52
+ spec.add_development_dependency 'parallel_tests', '~> 3.7'
52
53
  if !RUBY_VERSION.match(/^2\.[0123]/)
53
- spec.add_development_dependency 'pry'
54
- spec.add_development_dependency 'pry-byebug'
54
+ spec.add_development_dependency 'pry', '~> 0.14.1'
55
+ spec.add_development_dependency 'pry-byebug', '~> 3.8.0'
55
56
  end
56
57
  end
58
+
59
+ if RUBY_VERSION.match(/^3\./)
60
+ spec.add_development_dependency 'webrick', '~> 1.7.0'
61
+ end
57
62
  end
data/lib/ably/agent.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Ably
2
+ AGENT = "ably-ruby/#{Ably::VERSION} ruby/#{RUBY_VERSION}"
3
+ end
@@ -52,6 +52,12 @@ module Ably
52
52
  end
53
53
  end
54
54
 
55
+ # Maximum frame size exceeded TO3l9
56
+ class MaxFrameSizeExceeded < BaseAblyException; end
57
+
58
+ # Maximum message size exceeded TO3l8
59
+ class MaxMessageSizeExceeded < BaseAblyException; end
60
+
55
61
  # An invalid request was received by Ably
56
62
  class InvalidRequest < BaseAblyException; end
57
63
 
@@ -21,6 +21,12 @@ module Ably::Models
21
21
  class ConnectionDetails
22
22
  include Ably::Modules::ModelCommon
23
23
 
24
+ # Max message size
25
+ MAX_MESSAGE_SIZE = 65536 # See spec TO3l8
26
+
27
+ # Max frame size
28
+ MAX_FRAME_SIZE = 524288 # See spec TO3l9
29
+
24
30
  # @param attributes [Hash]
25
31
  # @option attributes [String] :client_id contains the client ID assigned to the connection
26
32
  # @option attributes [String] :connection_key the connection secret key string that is used to resume a connection and its state
@@ -38,6 +44,8 @@ module Ably::Models
38
44
  self.attributes[duration_field] = (self.attributes[duration_field].to_f / 1000).round
39
45
  end
40
46
  end
47
+ self.attributes[:max_message_size] ||= MAX_MESSAGE_SIZE
48
+ self.attributes[:max_frame_size] ||= MAX_FRAME_SIZE
41
49
  self.attributes.freeze
42
50
  end
43
51
 
@@ -0,0 +1,29 @@
1
+ module Ably::Models
2
+ #
3
+ # @!attribute [r] from
4
+ # @return [String] The id of the message the delta was generated from
5
+ # @!attribute [r] format
6
+ # @return [String] The delta format. Only vcdiff is supported as at API version 1.2
7
+ #
8
+ class DeltaExtras
9
+ include Ably::Modules::ModelCommon
10
+
11
+ # The id of the message the delta was generated from.
12
+ # @return [String, nil]
13
+ #
14
+ attr_reader :from
15
+
16
+ # The delta format.
17
+ # @return [String, nil]
18
+ #
19
+ attr_reader :format
20
+
21
+ def initialize(attributes = {})
22
+ @from, @format = IdiomaticRubyWrapper((attributes || {}), stop_at: [:from, :format]).attributes.values_at(:from, :format)
23
+ end
24
+
25
+ def to_json(*args)
26
+ as_json(args).to_json
27
+ end
28
+ end
29
+ end
@@ -27,6 +27,10 @@ module Ably::Models
27
27
  # @return [Integer] Ably error code (see ably-common/protocol/errors.json)
28
28
  # @!attribute [r] status
29
29
  # @return [Integer] HTTP Status Code corresponding to this error, where applicable
30
+ # @!attribute [r] request_id
31
+ # @return [Integer] HTTP RequestId corresponding to this error, where applicable (#RSC7c)
32
+ # @!attribute [r] cause
33
+ # @return [Integer] HTTP Status Code corresponding to this error, where applicable (#TI1)
30
34
  # @!attribute [r] attributes
31
35
  # @return [Hash] Access the protocol message Hash object ruby'fied to use symbolized keys
32
36
  #
@@ -38,7 +42,7 @@ module Ably::Models
38
42
  @hash_object = IdiomaticRubyWrapper(hash_object.clone.freeze)
39
43
  end
40
44
 
41
- %w(message code href status_code).each do |attribute|
45
+ %w(message code href status_code request_id cause).each do |attribute|
42
46
  define_method attribute do
43
47
  attributes[attribute.to_sym]
44
48
  end
@@ -52,7 +56,7 @@ module Ably::Models
52
56
  def to_s
53
57
  error_href = href || (code ? "https://help.ably.io/error/#{code}" : '')
54
58
  see_msg = " -> see #{error_href} for help" unless message.to_s.include?(error_href.to_s)
55
- "<Error: #{message} (code: #{code}, http status: #{status})>#{see_msg}"
59
+ "<Error: #{message} (code: #{code}, http status: #{status} request_id: #{request_id} cause: #{cause})>#{see_msg}"
56
60
  end
57
61
  end
58
62
  end
@@ -105,6 +105,20 @@ module Ably::Models
105
105
  end.to_json
106
106
  end
107
107
 
108
+ # The size is the sum over name, data, clientId, and extras in bytes (TO3l8a)
109
+ #
110
+ def size
111
+ %w(name data client_id extras).map do |attr|
112
+ if (value = attributes[attr.to_sym]).is_a?(String)
113
+ value.bytesize
114
+ elsif value.nil?
115
+ 0
116
+ else
117
+ value.to_json.bytesize
118
+ end
119
+ end.sum
120
+ end
121
+
108
122
  # Assign this message to a ProtocolMessage before delivery to the Ably system
109
123
  # @api private
110
124
  def assign_to_protocol_message(protocol_message)
@@ -128,6 +142,9 @@ module Ably::Models
128
142
 
129
143
  # Contains any arbitrary key value pairs which may also contain other primitive JSON types, JSON-encodable objects or JSON-encodable arrays.
130
144
  # The extras field is provided to contain message metadata and/or ancillary payloads in support of specific functionality, e.g. push
145
+ # 1.2 adds the delta extension which is of type DeltaExtras, and the headers extension, which contains arbitrary string->string key-value pairs,
146
+ # settable at publish time. Unless otherwise specified, the client library should not attempt to do any filtering or validation of the extras
147
+ # field itself, but should treat it opaquely, encoding it and passing it to realtime unaltered.
131
148
  # @api private
132
149
  def extras
133
150
  attributes[:extras].tap do |val|
@@ -137,6 +154,14 @@ module Ably::Models
137
154
  end
138
155
  end
139
156
 
157
+ # Delta extras extension (TM2i)
158
+ # @return [DeltaExtras, nil]
159
+ # @api private
160
+ def delta_extras
161
+ return nil if attributes[:extras][:delta].nil?
162
+ @delta_extras ||= DeltaExtras.new(attributes[:extras][:delta]).freeze
163
+ end
164
+
140
165
  private
141
166
  def raw_hash_object
142
167
  @raw_hash_object
@@ -125,6 +125,20 @@ module Ably::Models
125
125
  end.to_json
126
126
  end
127
127
 
128
+ # The size is the sum over data and clientId in bytes (TO3l8a)
129
+ #
130
+ def size
131
+ %w(data client_id).map do |attr|
132
+ if (value = attributes[attr.to_sym]).is_a?(String)
133
+ value.bytesize
134
+ elsif value.nil?
135
+ 0
136
+ else
137
+ value.to_json.bytesize
138
+ end
139
+ end.sum
140
+ end
141
+
128
142
  # Assign this presence message to a ProtocolMessage before delivery to the Ably system
129
143
  # @api private
130
144
  def assign_to_protocol_message(protocol_message)
@@ -19,8 +19,6 @@ module Ably::Models
19
19
  # @!attribute [r] channel_serial
20
20
  # @return [String] Contains a serial number for a message on the current channel
21
21
  # @!attribute [r] connection_id
22
- # @return [String] Contains a string public identifier for the connection
23
- # @!attribute [r] connection_key
24
22
  # @return [String] Contains a string private connection key used to recover this connection
25
23
  # @!attribute [r] connection_serial
26
24
  # @return [Bignum] Contains a serial number for a message sent from the server to the client
@@ -98,12 +96,6 @@ module Ably::Models
98
96
  end
99
97
  end
100
98
 
101
- def connection_key
102
- # connection_key in connection details takes precedence over connection_key on the ProtocolMessage
103
- # connection_key in the ProtocolMessage will be deprecated in future protocol versions > 0.8
104
- connection_details.connection_key || attributes[:connection_key]
105
- end
106
-
107
99
  def id!
108
100
  raise RuntimeError, 'ProtocolMessage #id is nil' unless id
109
101
  id
@@ -185,6 +177,14 @@ module Ably::Models
185
177
  end
186
178
  end
187
179
 
180
+ def message_size
181
+ presence.map(&:size).sum + messages.map(&:size).sum
182
+ end
183
+
184
+ def has_correct_message_size?
185
+ message_size <= connection_details.max_message_size
186
+ end
187
+
188
188
  def flags
189
189
  Integer(attributes[:flags])
190
190
  rescue TypeError
@@ -216,6 +216,11 @@ module Ably::Models
216
216
  flags & 16 == 16 # 2^4
217
217
  end
218
218
 
219
+ # @api private
220
+ def has_attach_resume_flag?
221
+ flags & 32 == 32 # 2^5
222
+ end
223
+
219
224
  # @api private
220
225
  def has_attach_presence_flag?
221
226
  flags & 65536 == 65536 # 2^16
@@ -6,8 +6,18 @@
6
6
  module Ably
7
7
  # Fallback hosts to use when a connection to rest/realtime.ably.io is not possible due to
8
8
  # network failures either at the client, between the client and Ably, within an Ably data center, or at the IO domain registrar
9
+ # see https://docs.ably.io/client-lib-development-guide/features/#RSC15a
9
10
  #
10
- FALLBACK_HOSTS = %w(A.ably-realtime.com B.ably-realtime.com C.ably-realtime.com D.ably-realtime.com E.ably-realtime.com).freeze
11
+ FALLBACK_DOMAIN = 'ably-realtime.com'.freeze
12
+ FALLBACK_IDS = %w(a b c d e).freeze
13
+
14
+ # Default production fallbacks a.ably-realtime.com ... e.ably-realtime.com
15
+ FALLBACK_HOSTS = FALLBACK_IDS.map { |host| "#{host}.#{FALLBACK_DOMAIN}".freeze }.freeze
16
+
17
+ # Custom environment default fallbacks {ENV}-a-fallback.ably-realtime.com ... {ENV}-a-fallback.ably-realtime.com
18
+ CUSTOM_ENVIRONMENT_FALLBACKS_SUFFIXES = FALLBACK_IDS.map do |host|
19
+ "-#{host}-fallback.#{FALLBACK_DOMAIN}".freeze
20
+ end.freeze
11
21
 
12
22
  INTERNET_CHECK = {
13
23
  url: '//internet-up.ably-realtime.com/is-the-internet-up.txt',
@@ -63,6 +63,8 @@ module Ably::Realtime
63
63
  log_channel_error protocol_message.error
64
64
  end
65
65
 
66
+ channel.properties.set_attach_serial(protocol_message.channel_serial)
67
+
66
68
  if protocol_message.has_channel_resumed_flag?
67
69
  logger.debug { "ChannelManager: Additional resumed ATTACHED message received for #{channel.state} channel '#{channel.name}'" }
68
70
  else
@@ -75,8 +77,6 @@ module Ably::Realtime
75
77
  )
76
78
  update_presence_sync_state_following_attached protocol_message
77
79
  end
78
-
79
- channel.properties.set_attach_serial(protocol_message.channel_serial)
80
80
  end
81
81
 
82
82
  # Handle DETACED messages, see #RTL13 for server-initated detaches
@@ -26,12 +26,16 @@ module Ably::Realtime
26
26
  transition :from => :detaching, :to => [:detached, :attaching, :attached, :failed, :suspended]
27
27
  transition :from => :detached, :to => [:attaching, :attached, :failed]
28
28
  transition :from => :suspended, :to => [:attaching, :attached, :detached, :failed]
29
- transition :from => :failed, :to => [:attaching]
29
+ transition :from => :failed, :to => [:attaching, :initialized]
30
30
 
31
31
  after_transition do |channel, transition|
32
32
  channel.synchronize_state_with_statemachine
33
33
  end
34
34
 
35
+ after_transition(to: [:initialized]) do |channel|
36
+ channel.clear_error_reason
37
+ end
38
+
35
39
  after_transition(to: [:attaching]) do |channel|
36
40
  channel.manager.attach
37
41
  end
@@ -22,6 +22,12 @@ module Ably::Realtime
22
22
  end
23
23
  end
24
24
 
25
+ max_message_size = connection.details && connection.details.max_message_size || Ably::Models::ConnectionDetails::MAX_MESSAGE_SIZE
26
+ if messages.sum(&:size) > max_message_size
27
+ error = Ably::Exceptions::MaxMessageSizeExceeded.new("Message size exceeded #{max_message_size} bytes.")
28
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
29
+ end
30
+
25
31
  connection.send_protocol_message(
26
32
  action: Ably::Models::ProtocolMessage::ACTION.Message.to_i,
27
33
  channel: channel_name,
@@ -323,6 +323,8 @@ module Ably
323
323
  def update_options(channel_options)
324
324
  @options = channel_options.clone.freeze
325
325
  end
326
+ alias set_options update_options # (RSL7)
327
+ alias options= update_options
326
328
 
327
329
  # Used by {Ably::Modules::StateEmitter} to debug state changes
328
330
  # @api private
@@ -121,15 +121,23 @@ module Ably::Realtime
121
121
  presence.manager.sync_process_messages protocol_message.channel_serial, protocol_message.presence
122
122
 
123
123
  when ACTION.Presence
124
- presence = get_channel(protocol_message.channel).presence
125
- protocol_message.presence.each do |presence_message|
126
- presence.__incoming_msgbus__.publish :presence, presence_message
124
+ if protocol_message.has_correct_message_size?
125
+ presence = get_channel(protocol_message.channel).presence
126
+ protocol_message.presence.each do |presence_message|
127
+ presence.__incoming_msgbus__.publish :presence, presence_message
128
+ end
129
+ else
130
+ logger.fatal Ably::Exceptions::ProtocolError.new("Not published. Channel message limit exceeded #{protocol_message.message_size} bytes", 400, Ably::Exceptions::Codes::UNABLE_TO_RECOVER_CHANNEL_MESSAGE_LIMIT_EXCEEDED).message
127
131
  end
128
132
 
129
133
  when ACTION.Message
130
- channel = get_channel(protocol_message.channel)
131
- protocol_message.messages.each do |message|
132
- channel.__incoming_msgbus__.publish :message, message
134
+ if protocol_message.has_correct_message_size?
135
+ channel = get_channel(protocol_message.channel)
136
+ protocol_message.messages.each do |message|
137
+ channel.__incoming_msgbus__.publish :message, message
138
+ end
139
+ else
140
+ logger.fatal Ably::Exceptions::ProtocolError.new("Not published. Channel message limit exceeded #{protocol_message.message_size} bytes", 400, Ably::Exceptions::Codes::UNABLE_TO_RECOVER_CHANNEL_MESSAGE_LIMIT_EXCEEDED).message
133
141
  end
134
142
 
135
143
  when ACTION.Auth
@@ -74,6 +74,7 @@ module Ably
74
74
  def_delegators :@rest_client, :use_tls?, :protocol, :protocol_binary?
75
75
  def_delegators :@rest_client, :environment, :custom_host, :custom_port, :custom_tls_port
76
76
  def_delegators :@rest_client, :log_level
77
+ def_delegators :@rest_client, :options
77
78
 
78
79
  # Creates a {Ably::Realtime::Client Realtime Client} and configures the {Ably::Auth} object for the connection.
79
80
  #
@@ -117,17 +117,17 @@ module Ably::Realtime
117
117
  EventMachine.next_tick { connection.trigger_resumed }
118
118
  resend_pending_message_ack_queue
119
119
  else
120
- logger.debug { "ConnectionManager: Connection was not resumed, old connection ID #{connection.id} has been updated with new connection ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}" }
120
+ logger.debug { "ConnectionManager: Connection was not resumed, old connection ID #{connection.id} has been updated with new connection ID #{protocol_message.connection_id} and key #{protocol_message.connection_details.connection_key}" }
121
121
  nack_messages_on_all_channels protocol_message.error
122
122
  force_reattach_on_channels protocol_message.error
123
123
  end
124
124
  else
125
- logger.debug { "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}" }
125
+ logger.debug { "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_details.connection_key}" }
126
126
  end
127
127
 
128
128
  reattach_suspended_channels protocol_message.error
129
129
 
130
- connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
130
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_details.connection_key, protocol_message.connection_serial
131
131
  end
132
132
 
133
133
  # When connection is CONNECTED and receives an update
@@ -139,7 +139,7 @@ module Ably::Realtime
139
139
  # Update the connection details and any associated defaults
140
140
  connection.set_connection_details protocol_message.connection_details
141
141
 
142
- connection.configure_new protocol_message.connection_id, protocol_message.connection_key, protocol_message.connection_serial
142
+ connection.configure_new protocol_message.connection_id, protocol_message.connection_details.connection_key, protocol_message.connection_serial
143
143
 
144
144
  state_change = Ably::Models::ConnectionStateChange.new(
145
145
  current: connection.state,
@@ -319,6 +319,15 @@ module Ably::Realtime
319
319
  end
320
320
  end
321
321
 
322
+ # @api private
323
+ def reintialize_failed_chanels
324
+ channels.select do |channel|
325
+ channel.failed?
326
+ end.each do |channel|
327
+ channel.transition_state_machine :initialized
328
+ end
329
+ end
330
+
322
331
  # When continuity on a connection is lost all messages
323
332
  # whether queued or awaiting an ACK must be NACK'd as we now have a new connection
324
333
  def nack_messages_on_all_channels(error)
@@ -36,6 +36,10 @@ module Ably::Realtime
36
36
  connection.manager.setup_transport
37
37
  end
38
38
 
39
+ after_transition(to: [:connecting], from: [:failed]) do |connection|
40
+ connection.manager.reintialize_failed_chanels
41
+ end
42
+
39
43
  after_transition(to: [:connecting], from: [:disconnected, :suspended]) do |connection|
40
44
  connection.manager.reconnect_transport
41
45
  end
@@ -292,7 +292,7 @@ module Ably
292
292
  def internet_up?
293
293
  url = "http#{'s' if client.use_tls?}:#{Ably::INTERNET_CHECK.fetch(:url)}"
294
294
  EventMachine::DefaultDeferrable.new.tap do |deferrable|
295
- EventMachine::HttpRequest.new(url).get.tap do |http|
295
+ EventMachine::HttpRequest.new(url, tls: { verify_peer: true }).get.tap do |http|
296
296
  http.errback do
297
297
  yield false if block_given?
298
298
  deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unable to connect to #{url}", nil, Ably::Exceptions::Codes::CONNECTION_FAILED)
@@ -434,7 +434,7 @@ module Ably
434
434
  'format' => client.protocol,
435
435
  'echo' => client.echo_messages,
436
436
  'v' => Ably::PROTOCOL_VERSION,
437
- 'lib' => client.rest_client.lib_version_id,
437
+ 'agent' => client.rest_client.agent
438
438
  )
439
439
 
440
440
  # Use native websocket heartbeats if possible, but allow Ably protocol heartbeats
@@ -52,8 +52,8 @@ module Ably
52
52
  #
53
53
  # # Publish an array of message Hashes
54
54
  # messages = [
55
- # { name: 'click', { x: 1, y: 2 } },
56
- # { name: 'click', { x: 2, y: 3 } }
55
+ # { name: 'click', data: { x: 1, y: 2 } },
56
+ # { name: 'click', data: { x: 2, y: 3 } }
57
57
  # ]
58
58
  # channel.publish messages
59
59
  #
@@ -85,7 +85,13 @@ module Ably
85
85
  [[{ name: first, data: second }.merge(third)], nil]
86
86
  end
87
87
 
88
- payload = messages.each_with_index.map do |message, index|
88
+ messages.map! { |message| Ably::Models::Message(message.dup) }
89
+
90
+ if messages.sum(&:size) > (max_message_size = client.max_message_size || Ably::Rest::Client::MAX_MESSAGE_SIZE)
91
+ raise Ably::Exceptions::MaxMessageSizeExceeded.new("Maximum message size exceeded #{max_message_size} bytes.")
92
+ end
93
+
94
+ payload = messages.map do |message|
89
95
  Ably::Models::Message(message.dup).tap do |msg|
90
96
  msg.encode client.encoders, options
91
97
 
@@ -161,6 +167,8 @@ module Ably
161
167
  def update_options(channel_options)
162
168
  @options = channel_options.clone.freeze
163
169
  end
170
+ alias set_options update_options # (RSL7)
171
+ alias options= update_options
164
172
 
165
173
  private
166
174
  def base_path
@@ -3,6 +3,9 @@ require 'json'
3
3
  require 'logger'
4
4
  require 'uri'
5
5
 
6
+ require 'typhoeus'
7
+ require 'typhoeus/adapters/faraday'
8
+
6
9
  require 'ably/rest/middleware/exceptions'
7
10
 
8
11
  module Ably
@@ -22,6 +25,9 @@ module Ably
22
25
  # Default Ably domain for REST
23
26
  DOMAIN = 'rest.ably.io'
24
27
 
28
+ MAX_MESSAGE_SIZE = 65536 # See spec TO3l8
29
+ MAX_FRAME_SIZE = 524288 # See spec TO3l8
30
+
25
31
  # Configuration for HTTP timeouts and HTTP request reattempts to fallback hosts
26
32
  HTTP_DEFAULTS = {
27
33
  open_timeout: 4,
@@ -49,6 +55,10 @@ module Ably
49
55
  # @return [Symbol]
50
56
  attr_reader :protocol
51
57
 
58
+ # Client agent i.e. `example-gem/1.2.0 ably-ruby/1.1.5 ruby/1.9.3`
59
+ # @return [String]
60
+ attr_reader :agent
61
+
52
62
  # {Ably::Auth} authentication object configured for this connection
53
63
  # @return [Ably::Auth]
54
64
  attr_reader :auth
@@ -109,6 +119,14 @@ module Ably
109
119
  # @return [Boolean]
110
120
  attr_reader :idempotent_rest_publishing
111
121
 
122
+ # Max message size (TO2, TO3l8) by default (65536 bytes) 64KiB
123
+ # @return [Integer]
124
+ attr_reader :max_message_size
125
+
126
+ # Max frame size (TO2, TO3l8) by default (524288 bytes) 512KiB
127
+ # @return [Integer]
128
+ attr_reader :max_frame_size
129
+
112
130
  # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
113
131
  #
114
132
  # @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key or Token ID
@@ -143,6 +161,8 @@ module Ably
143
161
  #
144
162
  # @option options [Boolean] :add_request_ids (false) When true, adds a unique request_id to each request sent to Ably servers. This is handy when reporting issues, because you can refer to a specific request.
145
163
  # @option options [Boolean] :idempotent_rest_publishing (false if ver < 1.2) When true, idempotent publishing is enabled for all messages published via REST
164
+ # @option options [Integer] :max_message_size (65536 bytes) Maximum size of all messages when publishing via REST publish()
165
+ # @option options [Integer] :max_frame_size (524288 bytes) Maximum size of frame
146
166
  #
147
167
  # @return [Ably::Rest::Client]
148
168
  #
@@ -165,6 +185,7 @@ module Ably
165
185
  end
166
186
  end
167
187
 
188
+ @agent = options.delete(:agent) || Ably::AGENT
168
189
  @realtime_client = options.delete(:realtime_client)
169
190
  @tls = options.delete(:tls) == false ? false : true
170
191
  @environment = options.delete(:environment) # nil is production
@@ -179,18 +200,21 @@ module Ably
179
200
  @add_request_ids = options.delete(:add_request_ids)
180
201
  @log_retries_as_info = options.delete(:log_retries_as_info)
181
202
  @idempotent_rest_publishing = options.delete(:idempotent_rest_publishing) || Ably.major_minor_version_numeric > 1.1
203
+ @max_message_size = options.delete(:max_message_size) || MAX_MESSAGE_SIZE
204
+ @max_frame_size = options.delete(:max_frame_size) || MAX_FRAME_SIZE
182
205
 
183
-
184
- if options[:fallback_hosts_use_default] && options[:fallback_jhosts]
185
- raise ArgumentError, "fallback_hosts_use_default cannot be set to trye when fallback_jhosts is also provided"
206
+ if options[:fallback_hosts_use_default] && options[:fallback_hosts]
207
+ raise ArgumentError, "fallback_hosts_use_default cannot be set to try when fallback_hosts is also provided"
186
208
  end
187
209
  @fallback_hosts = case
188
210
  when options.delete(:fallback_hosts_use_default)
189
211
  Ably::FALLBACK_HOSTS
190
212
  when options_fallback_hosts = options.delete(:fallback_hosts)
191
213
  options_fallback_hosts
192
- when environment || custom_host || options[:realtime_host] || custom_port || custom_tls_port
214
+ when custom_host || options[:realtime_host] || custom_port || custom_tls_port
193
215
  []
216
+ when environment
217
+ CUSTOM_ENVIRONMENT_FALLBACKS_SUFFIXES.map { |host| "#{environment}#{host}" }
194
218
  else
195
219
  Ably::FALLBACK_HOSTS
196
220
  end
@@ -202,6 +226,8 @@ module Ably
202
226
  @http_defaults = HTTP_DEFAULTS.dup
203
227
  options.each do |key, val|
204
228
  if http_key = key[/^http_(.+)/, 1]
229
+ # Typhoeus converts decimal durations to milliseconds, so 0.0001 timeout is treated as 0 (no timeout)
230
+ val = 0.001 if val.kind_of?(Numeric) && (val > 0) && (val < 0.001)
205
231
  @http_defaults[http_key.to_sym] = val if val && @http_defaults.has_key?(http_key.to_sym)
206
232
  end
207
233
  end
@@ -351,6 +377,9 @@ module Ably
351
377
  send_request(method, path, params, headers: headers)
352
378
  end
353
379
  when :post, :patch, :put
380
+ if body.to_json.bytesize > max_frame_size
381
+ raise Ably::Exceptions::MaxFrameSizeExceeded.new("Maximum frame size exceeded #{max_frame_size} bytes.")
382
+ end
354
383
  path_with_params = Addressable::URI.new
355
384
  path_with_params.query_values = params || {}
356
385
  query = path_with_params.query
@@ -466,16 +495,6 @@ module Ably
466
495
  end
467
496
  end
468
497
 
469
- # Library Ably version user agent
470
- # @api private
471
- def lib_version_id
472
- @lib_version_id ||= [
473
- 'ruby',
474
- Ably.lib_variant,
475
- Ably::VERSION
476
- ].compact.join('-')
477
- end
478
-
479
498
  # Allowable duration for an external auth request
480
499
  # For REST client this defaults to request_timeout
481
500
  # For Realtime clients this defaults to 250ms less than the realtime_request_timeout
@@ -656,7 +675,7 @@ module Ably
656
675
  accept: mime_type,
657
676
  user_agent: user_agent,
658
677
  'X-Ably-Version' => Ably::PROTOCOL_VERSION,
659
- 'X-Ably-Lib' => lib_version_id
678
+ 'Ably-Agent' => agent
660
679
  },
661
680
  request: {
662
681
  open_timeout: http_defaults.fetch(:open_timeout),
@@ -665,7 +684,7 @@ module Ably
665
684
  }
666
685
  end
667
686
 
668
- # Return a Faraday middleware stack to initiate the Faraday::Connection with
687
+ # Return a Faraday middleware stack to initiate the Faraday::RackBuilder with
669
688
  #
670
689
  # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
671
690
  def middleware
@@ -677,8 +696,8 @@ module Ably
677
696
 
678
697
  setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true
679
698
 
680
- # Set Faraday's HTTP adapter
681
- builder.adapter :excon
699
+ # Set Faraday's HTTP adapter with support for HTTP/2
700
+ builder.adapter :typhoeus, http_version: :httpv2_0
682
701
  end
683
702
  end
684
703
 
@@ -7,7 +7,10 @@ module Ably
7
7
  class FailIfUnsupportedMimeType < Faraday::Response::Middleware
8
8
  def on_complete(env)
9
9
  unless env.response_headers['Ably-Middleware-Parsed'] == true
10
- unless (500..599).include?(env.status)
10
+ # Ignore empty body with success status code for no body response
11
+ return if env.body.to_s.empty? && env.status == 204
12
+
13
+ unless (500..599).include?(env.status)
11
14
  raise Ably::Exceptions::InvalidResponseBody,
12
15
  "Content Type #{env.response_headers['Content-Type']} is not supported by this client library"
13
16
  end