ably-rest 1.0.6 → 1.1.4.rc

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +23 -15
  4. data/ably-rest.gemspec +6 -6
  5. data/lib/submodules/ably-ruby/.editorconfig +14 -0
  6. data/lib/submodules/ably-ruby/.travis.yml +10 -8
  7. data/lib/submodules/ably-ruby/CHANGELOG.md +75 -3
  8. data/lib/submodules/ably-ruby/LICENSE +1 -3
  9. data/lib/submodules/ably-ruby/README.md +12 -7
  10. data/lib/submodules/ably-ruby/Rakefile +32 -0
  11. data/lib/submodules/ably-ruby/SPEC.md +1277 -835
  12. data/lib/submodules/ably-ruby/ably.gemspec +15 -10
  13. data/lib/submodules/ably-ruby/lib/ably/auth.rb +30 -4
  14. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +10 -4
  15. data/lib/submodules/ably-ruby/lib/ably/logger.rb +7 -1
  16. data/lib/submodules/ably-ruby/lib/ably/models/channel_state_change.rb +1 -1
  17. data/lib/submodules/ably-ruby/lib/ably/models/connection_state_change.rb +1 -1
  18. data/lib/submodules/ably-ruby/lib/ably/models/device_details.rb +87 -0
  19. data/lib/submodules/ably-ruby/lib/ably/models/device_push_details.rb +86 -0
  20. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +23 -2
  21. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
  22. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +32 -2
  23. data/lib/submodules/ably-ruby/lib/ably/models/push_channel_subscription.rb +89 -0
  24. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +1 -1
  25. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +1 -1
  26. data/lib/submodules/ably-ruby/lib/ably/modules/exception_codes.rb +128 -0
  27. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +15 -2
  28. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -2
  29. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +1 -0
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +1 -1
  31. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +24 -102
  32. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +2 -6
  33. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  34. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/publisher.rb +74 -0
  35. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/push_channel.rb +62 -0
  36. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +91 -3
  37. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
  38. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  39. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +34 -20
  40. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +25 -9
  41. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  42. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +4 -4
  43. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +3 -3
  44. data/lib/submodules/ably-ruby/lib/ably/realtime/push.rb +40 -0
  45. data/lib/submodules/ably-ruby/lib/ably/realtime/push/admin.rb +61 -0
  46. data/lib/submodules/ably-ruby/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  47. data/lib/submodules/ably-ruby/lib/ably/realtime/push/device_registrations.rb +105 -0
  48. data/lib/submodules/ably-ruby/lib/ably/rest.rb +1 -0
  49. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +53 -17
  50. data/lib/submodules/ably-ruby/lib/ably/rest/channel/push_channel.rb +62 -0
  51. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +162 -35
  52. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -1
  53. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  54. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -0
  55. data/lib/submodules/ably-ruby/lib/ably/rest/push.rb +42 -0
  56. data/lib/submodules/ably-ruby/lib/ably/rest/push/admin.rb +54 -0
  57. data/lib/submodules/ably-ruby/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  58. data/lib/submodules/ably-ruby/lib/ably/rest/push/device_registrations.rb +103 -0
  59. data/lib/submodules/ably-ruby/lib/ably/version.rb +7 -2
  60. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +245 -17
  61. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +26 -20
  62. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +177 -59
  63. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +153 -0
  64. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +72 -6
  65. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +129 -18
  66. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +36 -34
  67. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +201 -167
  68. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_admin_spec.rb +736 -0
  69. data/lib/submodules/ably-ruby/spec/acceptance/realtime/push_spec.rb +27 -0
  70. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +41 -3
  71. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +2 -2
  72. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +79 -4
  73. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +6 -0
  74. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +129 -10
  75. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +158 -6
  76. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_admin_spec.rb +952 -0
  77. data/lib/submodules/ably-ruby/spec/acceptance/rest/push_spec.rb +25 -0
  78. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +1 -1
  79. data/lib/submodules/ably-ruby/spec/run_parallel_tests +33 -0
  80. data/lib/submodules/ably-ruby/spec/spec_helper.rb +1 -1
  81. data/lib/submodules/ably-ruby/spec/support/debug_failure_helper.rb +9 -5
  82. data/lib/submodules/ably-ruby/spec/support/test_app.rb +2 -2
  83. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +10 -3
  84. data/lib/submodules/ably-ruby/spec/unit/models/device_details_spec.rb +102 -0
  85. data/lib/submodules/ably-ruby/spec/unit/models/device_push_details_spec.rb +101 -0
  86. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +51 -3
  87. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +17 -2
  88. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +1 -1
  89. data/lib/submodules/ably-ruby/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  90. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +1 -1
  91. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +13 -1
  92. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +1 -1
  93. data/lib/submodules/ably-ruby/spec/unit/realtime/push_channel_spec.rb +36 -0
  94. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +8 -1
  95. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +30 -0
  96. data/lib/submodules/ably-ruby/spec/unit/rest/push_channel_spec.rb +36 -0
  97. metadata +46 -21
@@ -111,16 +111,12 @@ module Ably::Realtime
111
111
  end
112
112
  end
113
113
 
114
- # When a channel becomes detached, suspended or failed,
114
+ # When a channel becomes suspended or failed,
115
115
  # all queued messages should be failed immediately as we don't queue in
116
116
  # any of those states
117
117
  def fail_queued_messages(error)
118
118
  error = Ably::Exceptions::MessageDeliveryFailed.new("Queued messages on channel '#{channel.name}' in state '#{channel.state}' will never be delivered") unless error
119
119
  fail_messages_in_queue connection.__outgoing_message_queue__, error
120
- channel.__queue__.each do |message|
121
- nack_message message, error
122
- end
123
- channel.__queue__.clear
124
120
  end
125
121
 
126
122
  def fail_messages_in_queue(queue, error)
@@ -214,7 +210,7 @@ module Ably::Realtime
214
210
  state_at_time_of_request = channel.state
215
211
  @pending_state_change_timer = EventMachine::Timer.new(realtime_request_timeout) do
216
212
  if channel.state == state_at_time_of_request
217
- error = Ably::Models::ErrorInfo.new(code: 90007, message: "Channel #{new_state} operation failed (timed out)")
213
+ error = Ably::Models::ErrorInfo.new(code: Ably::Exceptions::Codes::CHANNEL_OPERATION_FAILED_NO_RESPONSE_FROM_SERVER, message: "Channel #{new_state} operation failed (timed out)")
218
214
  channel.transition_state_machine state_if_failed, reason: error
219
215
  end
220
216
  end
@@ -25,7 +25,7 @@ module Ably::Realtime
25
25
  transition :from => :attached, :to => [:attaching, :detaching, :detached, :failed, :suspended]
26
26
  transition :from => :detaching, :to => [:detached, :attaching, :attached, :failed, :suspended]
27
27
  transition :from => :detached, :to => [:attaching, :attached, :failed]
28
- transition :from => :suspended, :to => [:attaching, :detached, :failed]
28
+ transition :from => :suspended, :to => [:attaching, :attached, :detached, :failed]
29
29
  transition :from => :failed, :to => [:attaching]
30
30
 
31
31
  after_transition do |channel, transition|
@@ -47,7 +47,7 @@ module Ably::Realtime
47
47
 
48
48
  after_transition(to: [:detached, :failed, :suspended]) do |channel, current_transition|
49
49
  err = error_from_state_change(current_transition)
50
- channel.manager.fail_queued_messages err
50
+ channel.manager.fail_queued_messages(err) if channel.failed? or channel.suspended? #RTL11
51
51
  channel.manager.log_channel_error err if err
52
52
  end
53
53
 
@@ -0,0 +1,74 @@
1
+ module Ably::Realtime
2
+ class Channel
3
+ # Publisher module adds publishing capabilities to the current object
4
+ module Publisher
5
+ private
6
+
7
+ # Prepare and queue messages on the connection queue immediately
8
+ # @return [Ably::Util::SafeDeferrable]
9
+ def enqueue_messages_on_connection(client, raw_messages, channel_name, channel_options = {})
10
+ messages = Array(raw_messages).map do |raw_msg|
11
+ create_message(client, raw_msg, channel_options).tap do |message|
12
+ next if message.client_id.nil?
13
+ if message.client_id == '*'
14
+ raise Ably::Exceptions::IncompatibleClientId.new('Wildcard client_id is reserved and cannot be used when publishing messages')
15
+ end
16
+ if message.client_id && !message.client_id.kind_of?(String)
17
+ raise Ably::Exceptions::IncompatibleClientId.new('client_id must be a String when publishing messages')
18
+ end
19
+ unless client.auth.can_assume_client_id?(message.client_id)
20
+ raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{message.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'")
21
+ end
22
+ end
23
+ end
24
+
25
+ connection.send_protocol_message(
26
+ action: Ably::Models::ProtocolMessage::ACTION.Message.to_i,
27
+ channel: channel_name,
28
+ messages: messages
29
+ )
30
+
31
+ if messages.count == 1
32
+ # A message is a Deferrable so, if publishing only one message, simply return that Deferrable
33
+ messages.first
34
+ else
35
+ deferrable_for_multiple_messages(messages)
36
+ end
37
+ end
38
+
39
+ # A deferrable object that calls the success callback once all messages are delivered
40
+ # If any message fails, the errback is called immediately
41
+ # Only one callback or errback is ever called i.e. if a group of messages all fail, only once
42
+ # errback will be invoked
43
+ def deferrable_for_multiple_messages(messages)
44
+ expected_deliveries = messages.count
45
+ actual_deliveries = 0
46
+ failed = false
47
+
48
+ Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
49
+ messages.each do |message|
50
+ message.callback do
51
+ next if failed
52
+ actual_deliveries += 1
53
+ deferrable.succeed messages if actual_deliveries == expected_deliveries
54
+ end
55
+ message.errback do |error|
56
+ next if failed
57
+ failed = true
58
+ deferrable.fail error, message
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def create_message(client, message, channel_options)
65
+ Ably::Models::Message(message.dup).tap do |msg|
66
+ msg.encode(client.encoders, channel_options) do |encode_error, error_message|
67
+ client.logger.error error_message
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
@@ -0,0 +1,62 @@
1
+ module Ably::Realtime
2
+ class Channel
3
+ # A push channel used for push notifications
4
+ # Each PushChannel maps to exactly one Realtime Channel
5
+ #
6
+ # @!attribute [r] channel
7
+ # @return [Ably::Realtime::Channel] Underlying channel object
8
+ #
9
+ class PushChannel
10
+ attr_reader :channel
11
+
12
+ def initialize(channel)
13
+ raise ArgumentError, "Unsupported channel type '#{channel.class}'" unless channel.kind_of?(Ably::Realtime::Channel)
14
+ @channel = channel
15
+ end
16
+
17
+ def to_s
18
+ "<PushChannel: name=#{channel.name}>"
19
+ end
20
+
21
+ # Subscribe local device for push notifications on this channel
22
+ #
23
+ # @note This is unsupported in the Ruby library
24
+ def subscribe_device(*args)
25
+ raise_unsupported
26
+ end
27
+
28
+ # Subscribe all devices registered to this client's authenticated client_id for push notifications on this channel
29
+ #
30
+ # @note This is unsupported in the Ruby library
31
+ def subscribe_client_id(*args)
32
+ raise_unsupported
33
+ end
34
+
35
+ # Unsubscribe local device for push notifications on this channel
36
+ #
37
+ # @note This is unsupported in the Ruby library
38
+ def unsubscribe_device(*args)
39
+ raise_unsupported
40
+ end
41
+
42
+ # Unsubscribe all devices registered to this client's authenticated client_id for push notifications on this channel
43
+ #
44
+ # @note This is unsupported in the Ruby library
45
+ def unsubscribe_client_id(*args)
46
+ raise_unsupported
47
+ end
48
+
49
+ # Get list of subscriptions on this channel for this device or authenticate client_id
50
+ #
51
+ # @note This is unsupported in the Ruby library
52
+ def get_subscriptions(*args)
53
+ raise_unsupported
54
+ end
55
+
56
+ private
57
+ def raise_unsupported
58
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. All PushChannel methods are unavailable'
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,4 +1,5 @@
1
1
  require 'uri'
2
+ require 'ably/realtime/channel/publisher'
2
3
 
3
4
  module Ably
4
5
  module Realtime
@@ -21,6 +22,9 @@ module Ably
21
22
  #
22
23
  class Client
23
24
  include Ably::Modules::AsyncWrapper
25
+ include Ably::Realtime::Channel::Publisher
26
+ include Ably::Modules::Conversions
27
+
24
28
  extend Forwardable
25
29
 
26
30
  DOMAIN = 'realtime.ably.io'
@@ -38,6 +42,7 @@ module Ably
38
42
 
39
43
  # The {Ably::Rest::Client REST client} instantiated with the same credentials and configuration that is used for all REST operations such as authentication
40
44
  # @return [Ably::Rest::Client]
45
+ # @private
41
46
  attr_reader :rest_client
42
47
 
43
48
  # When false the client suppresses messages originating from this connection being echoed back on the same connection. Defaults to true
@@ -105,9 +110,6 @@ module Ably
105
110
  end
106
111
 
107
112
  @rest_client = Ably::Rest::Client.new(options.merge(realtime_client: self))
108
- @auth = Ably::Realtime::Auth.new(self)
109
- @channels = Ably::Realtime::Channels.new(self)
110
- @connection = Ably::Realtime::Connection.new(self, options)
111
113
  @echo_messages = rest_client.options.fetch(:echo_messages, true) == false ? false : true
112
114
  @queue_messages = rest_client.options.fetch(:queue_messages, true) == false ? false : true
113
115
  @custom_realtime_host = rest_client.options[:realtime_host] || rest_client.options[:ws_host]
@@ -115,6 +117,10 @@ module Ably
115
117
  @recover = rest_client.options[:recover]
116
118
 
117
119
  raise ArgumentError, "Recovery key '#{recover}' is invalid" if recover && !recover.match(Connection::RECOVER_REGEX)
120
+
121
+ @auth = Ably::Realtime::Auth.new(self)
122
+ @channels = Ably::Realtime::Channels.new(self)
123
+ @connection = Ably::Realtime::Connection.new(self, options)
118
124
  end
119
125
 
120
126
  # Return a {Ably::Realtime::Channel Realtime Channel} for the given name
@@ -162,6 +168,12 @@ module Ably
162
168
  connection.connect(&block)
163
169
  end
164
170
 
171
+ # Push notification object for publishing and managing push notifications
172
+ # @return [Ably::Realtime::Push]
173
+ def push
174
+ @push ||= Push.new(self)
175
+ end
176
+
165
177
  # (see Ably::Rest::Client#request)
166
178
  # @yield [Ably::Models::HttpPaginatedResponse<>] An Array of Stats
167
179
  #
@@ -172,6 +184,74 @@ module Ably
172
184
  end
173
185
  end
174
186
 
187
+ # Publish one or more messages to the specified channel.
188
+ #
189
+ # This method allows messages to be efficiently published to Ably without instancing a {Ably::Realtime::Channel} object.
190
+ # If you want to publish a high rate of messages to Ably without instancing channels or using the REST API, then this method
191
+ # is recommended. However, channel options such as encryption are not supported with this method. If you need to specify channel options
192
+ # we recommend you use the {Ably::Realtime::Channel} +publish+ method without attaching to each channel, unless you also want to subscribe
193
+ # to published messages on that channel.
194
+ #
195
+ # Note: This feature is still in beta. As such, we cannot guarantee the API will not change in future.
196
+ #
197
+ # @param channel [String] The channel name you want to publish the message(s) to
198
+ # @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
199
+ # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
200
+ # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string
201
+ #
202
+ # @yield [Ably::Models::Message,Array<Ably::Models::Message>] On success, will call the block with the {Ably::Models::Message} if a single message is published, or an Array of {Ably::Models::Message} when multiple messages are published
203
+ # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
204
+ #
205
+ # @example
206
+ # # Publish a single message
207
+ # client.publish 'activityChannel', click', { x: 1, y: 2 }
208
+ #
209
+ # # Publish an array of message Hashes
210
+ # messages = [
211
+ # { name: 'click', { x: 1, y: 2 } },
212
+ # { name: 'click', { x: 2, y: 3 } }
213
+ # ]
214
+ # client.publish 'activityChannel', messages
215
+ #
216
+ # # Publish an array of Ably::Models::Message objects
217
+ # messages = [
218
+ # Ably::Models::Message(name: 'click', { x: 1, y: 2 })
219
+ # Ably::Models::Message(name: 'click', { x: 2, y: 3 })
220
+ # ]
221
+ # client.publish 'activityChannel', messages
222
+ #
223
+ # client.publish('activityChannel', 'click', 'body') do |message|
224
+ # puts "#{message.name} event received with #{message.data}"
225
+ # end
226
+ #
227
+ # client.publish('activityChannel', 'click', 'body').errback do |error, message|
228
+ # puts "#{message.name} was not received, error #{error.message}"
229
+ # end
230
+ #
231
+ def publish(channel_name, name, data = nil, attributes = {}, &success_block)
232
+ if !connection.can_publish_messages?
233
+ error = Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. Client is not allowed to queue messages when connection is in state #{connection.state}")
234
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
235
+ end
236
+
237
+ messages = if name.kind_of?(Enumerable)
238
+ name
239
+ else
240
+ name = ensure_utf_8(:name, name, allow_nil: true)
241
+ ensure_supported_payload data
242
+ [{ name: name, data: data }.merge(attributes)]
243
+ end
244
+
245
+ if messages.length > Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE
246
+ error = Ably::Exceptions::InvalidRequest.new("It is not possible to publish more than #{Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE} messages with a single publish request.")
247
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
248
+ end
249
+
250
+ enqueue_messages_on_connection(self, messages, channel_name).tap do |deferrable|
251
+ deferrable.callback(&success_block) if block_given?
252
+ end
253
+ end
254
+
175
255
  # @!attribute [r] endpoint
176
256
  # @return [URI::Generic] Default Ably Realtime endpoint used for all requests
177
257
  def endpoint
@@ -214,6 +294,14 @@ module Ably
214
294
  @fallback_endpoints[fallback_endpoint_index % @fallback_endpoints.count]
215
295
  end
216
296
 
297
+ # The local device detilas
298
+ # @return [Ably::Models::LocalDevice]
299
+ #
300
+ # @note This is unsupported in the Ruby library
301
+ def device
302
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable'
303
+ end
304
+
217
305
  private
218
306
  def endpoint_for_host(host)
219
307
  port = if use_tls?
@@ -102,7 +102,11 @@ module Ably::Realtime
102
102
  if channel.attached?
103
103
  channel.manager.duplicate_attached_received protocol_message
104
104
  else
105
- channel.transition_state_machine :attached, reason: protocol_message.error, resumed: protocol_message.has_channel_resumed_flag?, protocol_message: protocol_message
105
+ if channel.failed?
106
+ logger.warn "Ably::Realtime::Client::IncomingMessageDispatcher - Received an ATTACHED protocol message for FAILED channel #{channel.name}. Ignoring ATTACHED message"
107
+ else
108
+ channel.transition_state_machine :attached, reason: protocol_message.error, resumed: protocol_message.has_channel_resumed_flag?, protocol_message: protocol_message
109
+ end
106
110
  end
107
111
  end
108
112
 
@@ -132,7 +136,7 @@ module Ably::Realtime
132
136
  client.auth.authorize
133
137
 
134
138
  else
135
- error = Ably::Exceptions::ProtocolError.new("Protocol Message Action #{protocol_message.action} is unsupported by this MessageDispatcher", 400, 80013)
139
+ error = Ably::Exceptions::ProtocolError.new("Protocol Message Action #{protocol_message.action} is unsupported by this MessageDispatcher", 400, Ably::Exceptions::Codes::PROTOCOL_ERROR)
136
140
  logger.fatal error.message
137
141
  end
138
142
  end
@@ -52,7 +52,7 @@ module Ably::Realtime
52
52
  protocol_message = outgoing_queue.shift
53
53
 
54
54
  if (!connection.transport)
55
- protocol_message.fail Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, 80003)
55
+ protocol_message.fail Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, Ably::Exceptions::Codes::DISCONNECTED)
56
56
  next
57
57
  end
58
58
 
@@ -66,7 +66,7 @@ module Ably
66
66
  ensure_state_machine_emits 'Ably::Models::ConnectionStateChange'
67
67
 
68
68
  # Expected format for a connection recover key
69
- RECOVER_REGEX = /^(?<recover>[\w!-]+):(?<connection_serial>\-?\w+)$/
69
+ RECOVER_REGEX = /^(?<recover>[^:]+):(?<connection_serial>[^:]+):(?<msg_serial>\-?\d+)$/
70
70
 
71
71
  # Defaults for automatic connection recovery and timeouts
72
72
  DEFAULTS = {
@@ -79,6 +79,9 @@ module Ably
79
79
  websocket_heartbeats_disabled: false,
80
80
  }.freeze
81
81
 
82
+ # Max number of messages to bundle in a single ProtocolMessage
83
+ MAX_PROTOCOL_MESSAGE_BATCH_SIZE = 50
84
+
82
85
  # A unique public identifier for this connection, used to identify this member in presence events and messages
83
86
  # @return [String]
84
87
  attr_reader :id
@@ -134,7 +137,6 @@ module Ably
134
137
  @client = client
135
138
  @__outgoing_message_queue__ = []
136
139
  @__pending_message_ack_queue__ = []
137
- reset_client_serial
138
140
 
139
141
  @defaults = DEFAULTS.dup
140
142
  options.each do |key, val|
@@ -142,12 +144,25 @@ module Ably
142
144
  end if options.kind_of?(Hash)
143
145
  @defaults.freeze
144
146
 
147
+ # If a recover client options is provided, then we need to ensure that the msgSerial matches the
148
+ # recover serial immediately at client library instantiation. This is done immediately so that any queued
149
+ # publishes use the correct serial number for these queued messages as well.
150
+ # There is no harm if the msgSerial is higher than expected if the recover fails.
151
+ recovery_msg_serial = connection_recover_parts && connection_recover_parts[:msg_serial].to_i
152
+ if recovery_msg_serial
153
+ @client_msg_serial = recovery_msg_serial
154
+ else
155
+ reset_client_msg_serial
156
+ end
157
+
145
158
  Client::IncomingMessageDispatcher.new client, self
146
159
  Client::OutgoingMessageDispatcher.new client, self
147
160
 
148
161
  @state_machine = ConnectionStateMachine.new(self)
149
162
  @state = STATE(state_machine.current_state)
150
163
  @manager = ConnectionManager.new(self)
164
+
165
+ @current_host = client.endpoint.host
151
166
  end
152
167
 
153
168
  # Causes the connection to close, entering the closed state, from any state except
@@ -225,7 +240,7 @@ module Ably
225
240
  #
226
241
  def ping(&block)
227
242
  if initialized? || suspended? || closing? || closed? || failed?
228
- error = Ably::Models::ErrorInfo.new(message: "Cannot send a ping when the connection is #{state}", code: 80003)
243
+ error = Ably::Models::ErrorInfo.new(message: "Cannot send a ping when the connection is #{state}", code: Ably::Exceptions::Codes::DISCONNECTED)
229
244
  return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
230
245
  end
231
246
 
@@ -256,7 +271,7 @@ module Ably
256
271
  once_or_if([:suspended, :closing, :closed, :failed]) do
257
272
  next if finished
258
273
  finished = true
259
- deferrable.fail Ably::Models::ErrorInfo.new(message: "Ping failed as connection has changed state to #{state}", code: 80003)
274
+ deferrable.fail Ably::Models::ErrorInfo.new(message: "Ping failed as connection has changed state to #{state}", code: Ably::Exceptions::Codes::DISCONNECTED)
260
275
  end
261
276
 
262
277
  EventMachine.add_timer(defaults.fetch(:realtime_request_timeout)) do
@@ -265,7 +280,7 @@ module Ably
265
280
  __incoming_protocol_msgbus__.unsubscribe(:protocol_message, &wait_for_ping)
266
281
  error_msg = "Ping timed out after #{defaults.fetch(:realtime_request_timeout)}s"
267
282
  logger.warn { error_msg }
268
- deferrable.fail Ably::Models::ErrorInfo.new(message: error_msg, code: 50003)
283
+ deferrable.fail Ably::Models::ErrorInfo.new(message: error_msg, code: Ably::Exceptions::Codes::TIMEOUT_ERROR)
269
284
  safe_yield block, nil if block_given?
270
285
  end
271
286
  end
@@ -280,7 +295,7 @@ module Ably
280
295
  EventMachine::HttpRequest.new(url).get.tap do |http|
281
296
  http.errback do
282
297
  yield false if block_given?
283
- deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unable to connect to #{url}", nil, 80000)
298
+ deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unable to connect to #{url}", nil, Ably::Exceptions::Codes::CONNECTION_FAILED)
284
299
  end
285
300
  http.callback do
286
301
  EventMachine.next_tick do
@@ -289,7 +304,7 @@ module Ably
289
304
  if result
290
305
  deferrable.succeed
291
306
  else
292
- deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unexpected response from #{url} (#{http.response_header.status})", 400, 40000)
307
+ deferrable.fail Ably::Exceptions::ConnectionFailed.new("Unexpected response from #{url} (#{http.response_header.status})", 400, Ably::Exceptions::Codes::BAD_REQUEST)
293
308
  end
294
309
  end
295
310
  end
@@ -300,18 +315,17 @@ module Ably
300
315
  # @!attribute [r] recovery_key
301
316
  # @return [String] recovery key that can be used by another client to recover this connection with the :recover option
302
317
  def recovery_key
303
- "#{key}:#{serial}" if connection_resumable?
318
+ "#{key}:#{serial}:#{client_msg_serial}" if connection_resumable?
304
319
  end
305
320
 
306
321
  # Following a new connection being made, the connection ID, connection key
307
- # and message serial need to match the details provided by the server.
322
+ # and connection serial need to match the details provided by the server.
308
323
  #
309
324
  # @return [void]
310
325
  # @api private
311
326
  def configure_new(connection_id, connection_key, connection_serial)
312
327
  @id = connection_id
313
328
  @key = connection_key
314
- @client_serial = connection_serial
315
329
 
316
330
  update_connection_serial connection_serial
317
331
  end
@@ -539,11 +553,11 @@ module Ably
539
553
  defaults.fetch(:realtime_request_timeout)
540
554
  end
541
555
 
542
- # Resets the client serial (msgSerial) sent to Ably for each new {Ably::Models::ProtocolMessage}
543
- # (see #client_serial)
556
+ # Resets the client message serial (msgSerial) sent to Ably for each new {Ably::Models::ProtocolMessage}
557
+ # (see #client_msg_serial)
544
558
  # @api private
545
- def reset_client_serial
546
- @client_serial = -1
559
+ def reset_client_msg_serial
560
+ @client_msg_serial = -1
547
561
  end
548
562
 
549
563
  # When a hearbeat or any other message from Ably is received
@@ -565,15 +579,15 @@ module Ably
565
579
 
566
580
  private
567
581
 
568
- # The client serial is incremented for every message that is published that requires an ACK.
582
+ # The client message serial (msgSerial) is incremented for every message that is published that requires an ACK.
569
583
  # Note that this is different to the connection serial that contains the last known serial number
570
584
  # received from the server.
571
585
  #
572
586
  # A message serial number does not guarantee a message has been received, only sent.
573
587
  # A connection serial guarantees the server has received the message and is thus used for connection recovery and resumes.
574
588
  # @return [Integer] starting at -1 indicating no messages sent, 0 when the first message is sent
575
- def client_serial
576
- @client_serial
589
+ def client_msg_serial
590
+ @client_msg_serial
577
591
  end
578
592
 
579
593
  def resume_callbacks
@@ -598,11 +612,11 @@ module Ably
598
612
  end
599
613
 
600
614
  def add_message_serial_to(protocol_message)
601
- @client_serial += 1
602
- protocol_message[:msgSerial] = client_serial
615
+ @client_msg_serial += 1
616
+ protocol_message[:msgSerial] = client_msg_serial
603
617
  yield
604
618
  rescue StandardError => e
605
- @client_serial -= 1
619
+ @client_msg_serial -= 1
606
620
  raise e
607
621
  end
608
622