ably 1.1.4 → 1.1.8

Sign up to get free protection for your applications and to get access to all the features.
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