ably-rest 0.8.2 → 0.8.3

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -43
  3. data/SPEC.md +707 -580
  4. data/lib/submodules/ably-ruby/.travis.yml +1 -0
  5. data/lib/submodules/ably-ruby/CHANGELOG.md +143 -3
  6. data/lib/submodules/ably-ruby/README.md +1 -1
  7. data/lib/submodules/ably-ruby/SPEC.md +842 -520
  8. data/lib/submodules/ably-ruby/ably.gemspec +1 -1
  9. data/lib/submodules/ably-ruby/lib/ably/auth.rb +114 -87
  10. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +40 -14
  11. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +3 -5
  12. data/lib/submodules/ably-ruby/lib/ably/models/paginated_result.rb +3 -12
  13. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +8 -2
  14. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +15 -3
  15. data/lib/submodules/ably-ruby/lib/ably/models/stat.rb +1 -1
  16. data/lib/submodules/ably-ruby/lib/ably/models/token_details.rb +1 -1
  17. data/lib/submodules/ably-ruby/lib/ably/modules/channels_collection.rb +7 -1
  18. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +1 -1
  19. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +6 -3
  20. data/lib/submodules/ably-ruby/lib/ably/modules/message_pack.rb +2 -2
  21. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +1 -1
  22. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +2 -2
  23. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +1 -0
  24. data/lib/submodules/ably-ruby/lib/ably/realtime/auth.rb +191 -0
  25. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +97 -25
  26. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +11 -3
  27. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +22 -6
  28. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +73 -40
  29. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +48 -33
  30. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +17 -3
  31. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +43 -16
  32. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +57 -26
  33. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +3 -1
  34. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -2
  35. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +1 -0
  36. data/lib/submodules/ably-ruby/lib/ably/version.rb +1 -1
  37. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +242 -0
  38. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +277 -5
  39. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channels_spec.rb +64 -0
  40. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +26 -5
  41. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +23 -6
  42. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +167 -16
  43. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +9 -8
  44. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +1 -0
  45. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +121 -10
  46. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +13 -1
  47. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +161 -79
  48. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +3 -3
  49. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +142 -15
  50. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +23 -0
  51. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +180 -18
  52. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +8 -8
  53. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +136 -25
  54. data/lib/submodules/ably-ruby/spec/acceptance/rest/stats_spec.rb +60 -4
  55. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +54 -3
  56. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +7 -6
  57. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +1 -9
  58. data/lib/submodules/ably-ruby/spec/unit/models/paginated_result_spec.rb +1 -18
  59. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +1 -1
  60. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +21 -1
  61. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +10 -3
  62. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +27 -8
  63. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +0 -8
  64. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +7 -7
  65. metadata +5 -2
@@ -92,12 +92,10 @@ module Ably::Models
92
92
  @hash_object
93
93
  end
94
94
 
95
- def as_json(*args)
96
- raise RuntimeError, ':name is missing, cannot generate a valid Hash for Message' unless name
97
-
98
- hash.dup.tap do |message|
95
+ def to_json(*args)
96
+ as_json(*args).tap do |message|
99
97
  decode_binary_data_before_to_json message
100
- end.as_json
98
+ end.to_json
101
99
  end
102
100
 
103
101
  # Assign this message to a ProtocolMessage before delivery to the Ably system
@@ -1,6 +1,6 @@
1
1
  module Ably::Models
2
2
  # Wraps any Ably HTTP response that supports paging and provides methods to iterate through
3
- # the pages using {#first}, {#next}, {#first?}, {#has_next?} and {#last?}
3
+ # the pages using {#first}, {#next}, {#has_next?} and {#last?}
4
4
  #
5
5
  # All items in the HTTP response are available in the Array returned from {#items}
6
6
  #
@@ -68,14 +68,6 @@ module Ably::Models
68
68
  pagination_header('next').nil?
69
69
  end
70
70
 
71
- # True if this is the first page in the paged resource set
72
- #
73
- # @return [Boolean]
74
- def first?
75
- !supports_pagination? ||
76
- pagination_header('first') == pagination_header('current')
77
- end
78
-
79
71
  # True if there is a subsequent page in this paginated set available with {#next}
80
72
  #
81
73
  # @return [Boolean]
@@ -94,8 +86,7 @@ module Ably::Models
94
86
  <<-EOF.gsub(/^ /, '')
95
87
  #<#{self.class.name}:#{self.object_id}
96
88
  @base_url="#{base_url}",
97
- @first?=#{!!first?},
98
- @last?=#{!!first?},
89
+ @last?=#{!!last?},
99
90
  @has_next?=#{!!has_next?},
100
91
  @items=
101
92
  #{items.map { |item| item.inspect }.join(",\n ") }
@@ -132,7 +123,7 @@ module Ably::Models
132
123
  end
133
124
 
134
125
  def pagination_url(id)
135
- raise Ably::Exceptions::InvalidPageError, "Paging header link #{id} does not exist" unless pagination_header(id)
126
+ raise Ably::Exceptions::PageMissing, "Paging header link #{id} does not exist" unless pagination_header(id)
136
127
 
137
128
  if pagination_header(id).match(%r{^\./})
138
129
  "#{base_url}#{pagination_header(id)[2..-1]}"
@@ -109,12 +109,18 @@ module Ably::Models
109
109
  def as_json(*args)
110
110
  hash.dup.tap do |presence_message|
111
111
  presence_message['action'] = action.to_i
112
- decode_binary_data_before_to_json presence_message
113
- end.as_json
112
+ end.as_json.reject { |key, val| val.nil? }
114
113
  rescue KeyError
115
114
  raise KeyError, ':action is missing or invalid, cannot generate a valid Hash for ProtocolMessage'
116
115
  end
117
116
 
117
+ def to_json(*args)
118
+ as_json(*args).tap do |presence_message|
119
+ decode_binary_data_before_to_json presence_message
120
+ end.to_json
121
+ end
122
+
123
+
118
124
  # Assign this presence message to a ProtocolMessage before delivery to the Ably system
119
125
  # @api private
120
126
  def assign_to_protocol_message(protocol_message)
@@ -27,9 +27,9 @@ module Ably::Models
27
27
  # @!attribute [r] timestamp
28
28
  # @return [Time] An optional timestamp, applied by the service in messages sent to the client, to indicate the system time at which the message was sent (milliseconds past epoch)
29
29
  # @!attribute [r] messages
30
- # @return [Message] A {ProtocolMessage} with a `:message` action contains one or more messages belonging to a channel
30
+ # @return [Array<Message>] A {ProtocolMessage} with a `:message` action contains one or more messages belonging to the channel
31
31
  # @!attribute [r] presence
32
- # @return [PresenceMessage] A {ProtocolMessage} with a `:presence` action contains one or more presence updates belonging to a channel
32
+ # @return [Array<PresenceMessage>] A {ProtocolMessage} with a `:presence` action contains one or more presence updates belonging to the channel
33
33
  # @!attribute [r] flags
34
34
  # @return [Integer] Flags indicating special ProtocolMessage states
35
35
  # @!attribute [r] hash
@@ -37,6 +37,7 @@ module Ably::Models
37
37
  #
38
38
  class ProtocolMessage
39
39
  include Ably::Modules::ModelCommon
40
+ include Ably::Modules::Encodeable
40
41
  include Ably::Modules::SafeDeferrable if defined?(Ably::Realtime)
41
42
  extend Ably::Modules::Enum
42
43
 
@@ -203,7 +204,18 @@ module Ably::Models
203
204
  end
204
205
 
205
206
  def to_s
206
- to_json
207
+ json_hash = as_json
208
+
209
+ # Decode any binary data to before converting to a JSON string representation
210
+ %w(messages presence).each do |message_type|
211
+ if json_hash[message_type] && !json_hash[message_type].empty?
212
+ json_hash[message_type].each do |message|
213
+ decode_binary_data_before_to_json message
214
+ end
215
+ end
216
+ end
217
+
218
+ json_hash.to_json
207
219
  end
208
220
 
209
221
  # True if the ProtocolMessage appears to be invalid, however this is not a guarantee
@@ -191,7 +191,7 @@ module Ably::Models
191
191
  end
192
192
 
193
193
  def as_json(*args)
194
- hash.as_json(*args)
194
+ hash.as_json(*args).reject { |key, val| val.nil? }
195
195
  end
196
196
 
197
197
  private
@@ -25,7 +25,7 @@ module Ably::Models
25
25
 
26
26
  # Buffer in seconds before a token is considered unusable
27
27
  # For example, if buffer is 10s, the token can no longer be used for new requests 9s before it expires
28
- TOKEN_EXPIRY_BUFFER = 5
28
+ TOKEN_EXPIRY_BUFFER = 15
29
29
 
30
30
  def initialize(attributes)
31
31
  @hash_object = IdiomaticRubyWrapper(attributes.clone.freeze)
@@ -18,7 +18,13 @@ module Ably::Modules
18
18
  # @return [Channel]
19
19
  #
20
20
  def get(name, channel_options = {})
21
- channels[name] ||= channel_klass.new(client, name, channel_options)
21
+ if channels.has_key?(name)
22
+ channels[name].tap do |channel|
23
+ channel.update_options channel_options if channel_options && !channel_options.empty?
24
+ end
25
+ else
26
+ channels[name] ||= channel_klass.new(client, name, channel_options)
27
+ end
22
28
  end
23
29
  alias_method :[], :get
24
30
 
@@ -109,7 +109,7 @@ module Ably::Modules
109
109
  payload.kind_of?(Array) ||
110
110
  payload.nil?
111
111
 
112
- raise Ably::Exceptions::UnsupportedDataTypeError.new('Invalid data payload', 400, 40011)
112
+ raise Ably::Exceptions::UnsupportedDataType.new('Invalid data payload', 400, 40011)
113
113
  end
114
114
  end
115
115
  end
@@ -35,9 +35,12 @@ module Ably::Modules
35
35
 
36
36
  private
37
37
  def decode_binary_data_before_to_json(message)
38
- if message[:data].kind_of?(String) && message[:data].encoding == ::Encoding::ASCII_8BIT
39
- message[:data] = ::Base64.encode64(message[:data])
40
- message[:encoding] = [message[:encoding], 'base64'].compact.join('/')
38
+ data_key = message[:data] ? :data : 'data'
39
+ encoding_key = message[:encoding] ? :encoding : 'encoding'
40
+
41
+ if message[data_key].kind_of?(String) && message[data_key].encoding == ::Encoding::ASCII_8BIT
42
+ message[data_key] = ::Base64.encode64(message[data_key])
43
+ message[encoding_key] = [message[encoding_key], 'base64'].compact.join('/')
41
44
  end
42
45
  end
43
46
 
@@ -7,8 +7,8 @@ module Ably::Modules
7
7
  module MessagePack
8
8
  # Generate a packed MsgPack version of this object based on the JSON representation.
9
9
  # Keys thus use mixedCase syntax as expected by the Realtime API
10
- def to_msgpack(*args)
11
- as_json(*args).to_msgpack
10
+ def to_msgpack(pk = nil)
11
+ as_json.to_msgpack(pk)
12
12
  end
13
13
  end
14
14
  end
@@ -22,7 +22,7 @@ module Ably::Modules
22
22
 
23
23
  # Return a JSON ready object from the underlying #hash using Ably naming conventions for keys
24
24
  def as_json
25
- hash.as_json.dup
25
+ hash.as_json.reject { |key, val| val.nil? }
26
26
  end
27
27
 
28
28
  # Stringify the JSON representation of this object from the underlying #hash
@@ -40,10 +40,10 @@ module Ably::Modules
40
40
  previous_transition.to_state if previous_transition
41
41
  end
42
42
 
43
- # @return [Ably::Exceptions::StateChangeError]
43
+ # @return [Ably::Exceptions::InvalidStateChange]
44
44
  def exception_for_state_change_to(state)
45
45
  error_message = "#{self.class}: Unable to transition from #{current_state} => #{state}"
46
- Ably::Exceptions::StateChangeError.new(error_message, nil, 80020)
46
+ Ably::Exceptions::InvalidStateChange.new(error_message, nil, 80020)
47
47
  end
48
48
 
49
49
  module ClassMethods
@@ -3,6 +3,7 @@ require 'websocket/driver'
3
3
 
4
4
  require 'ably/modules/event_emitter'
5
5
 
6
+ require 'ably/realtime/auth'
6
7
  require 'ably/realtime/channel'
7
8
  require 'ably/realtime/channels'
8
9
  require 'ably/realtime/client'
@@ -0,0 +1,191 @@
1
+ require 'ably/auth'
2
+
3
+ module Ably
4
+ module Realtime
5
+ # Auth is responsible for authentication with {https://www.ably.io Ably} using basic or token authentication
6
+ # This {Ably::Realtime::Auth Realtime::Auth} class wraps the {Ably::Auth Synchronous Ably::Auth} class in an EventMachine friendly way using Deferrables for all IO. See {Ably::Auth Ably::Auth} for more information
7
+ #
8
+ # Find out more about Ably authentication at: https://www.ably.io/documentation/general/authentication/
9
+ #
10
+ # @!attribute [r] client_id
11
+ # (see Ably::Auth#client_id)
12
+ # @!attribute [r] current_token_details
13
+ # (see Ably::Auth#current_token_details)
14
+ # @!attribute [r] token
15
+ # (see Ably::Auth#token)
16
+ # @!attribute [r] key
17
+ # (see Ably::Auth#key)
18
+ # @!attribute [r] key_name
19
+ # (see Ably::Auth#key_name)
20
+ # @!attribute [r] key_secret
21
+ # (see Ably::Auth#key_secret)
22
+ # @!attribute [r] options
23
+ # (see Ably::Auth#options)
24
+ # @!attribute [r] token_params
25
+ # (see Ably::Auth#options)
26
+ # @!attribute [r] using_basic_auth?
27
+ # (see Ably::Auth#using_basic_auth?)
28
+ # @!attribute [r] using_token_auth?
29
+ # (see Ably::Auth#using_token_auth?)
30
+ # @!attribute [r] token_renewable?
31
+ # (see Ably::Auth#token_renewable?)
32
+ # @!attribute [r] authentication_security_requirements_met?
33
+ # (see Ably::Auth#authentication_security_requirements_met?)
34
+ #
35
+ class Auth
36
+ extend Forwardable
37
+ include Ably::Modules::AsyncWrapper
38
+
39
+ def_delegators :auth_sync, :client_id
40
+ def_delegators :auth_sync, :current_token_details, :token
41
+ def_delegators :auth_sync, :key, :key_name, :key_secret, :options, :auth_options, :token_params
42
+ def_delegators :auth_sync, :using_basic_auth?, :using_token_auth?
43
+ def_delegators :auth_sync, :token_renewable?, :authentication_security_requirements_met?
44
+ def_delegators :client, :logger
45
+
46
+ def initialize(client)
47
+ @client = client
48
+ @auth_sync = client.rest_client.auth
49
+ end
50
+
51
+ # Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary.
52
+ #
53
+ # In the event that a new token request is made, the provided options are used
54
+ #
55
+ # @param (see Ably::Auth#authorise)
56
+ # @option (see Ably::Auth#authorise)
57
+ #
58
+ # @return [Ably::Util::SafeDeferrable]
59
+ # @yield [Ably::Models::TokenDetails]
60
+ #
61
+ # @example
62
+ # # will issue a simple token request using basic auth
63
+ # client = Ably::Rest::Client.new(key: 'key.id:secret')
64
+ # client.auth.authorise do |token_details|
65
+ # token_details #=> Ably::Models::TokenDetails
66
+ # end
67
+ #
68
+ def authorise(auth_options = {}, token_params = {}, &success_callback)
69
+ async_wrap(success_callback) do
70
+ auth_sync.authorise(auth_options, token_params)
71
+ end
72
+ end
73
+
74
+ # Synchronous version of {#authorise}. See {Ably::Auth#authorise} for method definition
75
+ # @param (see Ably::Auth#authorise)
76
+ # @option (see Ably::Auth#authorise)
77
+ # @return [Ably::Models::TokenDetails]
78
+ #
79
+ def authorise_sync(auth_options = {}, token_params = {})
80
+ auth_sync.authorise(auth_options, token_params)
81
+ end
82
+
83
+ # def_delegator :auth_sync, :request_token, :request_token_sync
84
+ # def_delegator :auth_sync, :create_token_request, :create_token_request_sync
85
+ # def_delegator :auth_sync, :auth_header, :auth_header_sync
86
+ # def_delegator :auth_sync, :auth_params, :auth_params_sync
87
+
88
+ # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests
89
+ #
90
+ # @param (see Ably::Auth#request_token)
91
+ # @option (see Ably::Auth#request_token)
92
+ #
93
+ # @return [Ably::Util::SafeDeferrable]
94
+ # @yield [Ably::Models::TokenDetails]
95
+ #
96
+ # @example
97
+ # # simple token request using basic auth
98
+ # client = Ably::Rest::Client.new(key: 'key.id:secret')
99
+ # client.auth.request_token do |token_details|
100
+ # token_details #=> Ably::Models::TokenDetails
101
+ # end
102
+ #
103
+ def request_token(auth_options = {}, token_params = {}, &success_callback)
104
+ async_wrap(success_callback) do
105
+ request_token_sync(auth_options, token_params)
106
+ end
107
+ end
108
+
109
+ # Synchronous version of {#request_token}. See {Ably::Auth#request_token} for method definition
110
+ # @param (see Ably::Auth#authorise)
111
+ # @option (see Ably::Auth#authorise)
112
+ # @return [Ably::Models::TokenDetails]
113
+ #
114
+ def request_token_sync(auth_options = {}, token_params = {})
115
+ auth_sync.request_token(auth_options, token_params)
116
+ end
117
+
118
+ # Creates and signs a token request that can then subsequently be used by any client to request a token
119
+ #
120
+ # @param (see Ably::Auth#create_token_request)
121
+ # @option (see Ably::Auth#create_token_request)
122
+ #
123
+ # @return [Ably::Util::SafeDeferrable]
124
+ # @yield [Models::TokenRequest]
125
+ #
126
+ # @example
127
+ # client.auth.create_token_request(id: 'asd.asd', ttl: 3600) do |token_request|
128
+ # token_request #=> Ably::Models::TokenRequest
129
+ # end
130
+ def create_token_request(auth_options = {}, token_params = {}, &success_callback)
131
+ async_wrap(success_callback) do
132
+ create_token_request_sync(auth_options, token_params)
133
+ end
134
+ end
135
+
136
+ # Synchronous version of {#create_token_request}. See {Ably::Auth#create_token_request} for method definition
137
+ # @param (see Ably::Auth#authorise)
138
+ # @option (see Ably::Auth#authorise)
139
+ # @return [Ably::Models::TokenRequest]
140
+ #
141
+ def create_token_request_sync(auth_options = {}, token_params = {})
142
+ auth_sync.create_token_request(auth_options, token_params)
143
+ end
144
+
145
+ # Auth header string used in HTTP requests to Ably
146
+ # Will reauthorise implicitly if required and capable
147
+ #
148
+ # @return [Ably::Util::SafeDeferrable]
149
+ # @yield [String] HTTP authentication value used in HTTP_AUTHORIZATION header
150
+ #
151
+ def auth_header(&success_callback)
152
+ async_wrap(success_callback) do
153
+ auth_header_sync
154
+ end
155
+ end
156
+
157
+ # Synchronous version of {#auth_header}. See {Ably::Auth#auth_header} for method definition
158
+ # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header
159
+ #
160
+ def auth_header_sync
161
+ auth_sync.auth_header
162
+ end
163
+
164
+ # Auth params used in URI endpoint for Realtime connections
165
+ # Will reauthorise implicitly if required and capable
166
+ #
167
+ # @return [Ably::Util::SafeDeferrable]
168
+ # @yield [Hash] Auth params for a new Realtime connection
169
+ #
170
+ def auth_params(&success_callback)
171
+ async_wrap(success_callback) do
172
+ auth_params_sync
173
+ end
174
+ end
175
+
176
+ # Synchronous version of {#auth_params}. See {Ably::Auth#auth_params} for method definition
177
+ # @return [Hash] Auth params for a new Realtime connection
178
+ #
179
+ def auth_params_sync
180
+ auth_sync.auth_params
181
+ end
182
+
183
+ private
184
+ # The synchronous Auth class instanced by the Rest client
185
+ # @return [Ably::Auth]
186
+ attr_reader :auth_sync
187
+
188
+ attr_reader :client
189
+ end
190
+ end
191
+ end
@@ -87,9 +87,9 @@ module Ably
87
87
  def initialize(client, name, channel_options = {})
88
88
  ensure_utf_8 :name, name
89
89
 
90
+ update_options channel_options
90
91
  @client = client
91
92
  @name = name
92
- @options = channel_options.clone.freeze
93
93
  @queue = []
94
94
 
95
95
  @state_machine = ChannelStateMachine.new(self)
@@ -100,17 +100,33 @@ module Ably
100
100
  setup_presence
101
101
  end
102
102
 
103
- # Publish a message on the channel.
103
+ # Publish one or more messages to the channel.
104
104
  #
105
105
  # When publishing a message, if the channel is not attached, the channel is implicitly attached
106
106
  #
107
- # @param name [String] The event name of the message
108
- # @param data [String,ByteArray] payload for the message
109
- # @yield [Ably::Models::Message] On success, will call the block with the {Ably::Models::Message}
110
- # @return [Ably::Models::Message] Deferrable {Ably::Models::Message} that supports both success (callback) and failure (errback) callbacks
107
+ # @param name [String, Array<Ably::Models::Message|Hash>, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs
108
+ # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
109
+ #
110
+ # @yield [Ably::Models::Message,Array<Ably::Models::Message>] On success, will call the block with the {Ably::Models::Message} if a single message is publishde, or an Array of {Ably::Models::Message} when multiple messages are published
111
+ # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
111
112
  #
112
113
  # @example
113
- # channel.publish('click', 'body')
114
+ # # Publish a single message
115
+ # channel.publish 'click', { x: 1, y: 2 }
116
+ #
117
+ # # Publish an array of message Hashes
118
+ # messages = [
119
+ # { name: 'click', { x: 1, y: 2 } },
120
+ # { name: 'click', { x: 2, y: 3 } }
121
+ # ]
122
+ # channel.publish messages
123
+ #
124
+ # # Publish an array of Ably::Models::Message objects
125
+ # messages = [
126
+ # Ably::Models::Message(name: 'click', { x: 1, y: 2 })
127
+ # Ably::Models::Message(name: 'click', { x: 2, y: 3 })
128
+ # ]
129
+ # channel.publish messages
114
130
  #
115
131
  # channel.publish('click', 'body') do |message|
116
132
  # puts "#{message.name} event received with #{message.data}"
@@ -120,13 +136,24 @@ module Ably
120
136
  # puts "#{message.name} was not received, error #{error.message}"
121
137
  # end
122
138
  #
123
- def publish(name, data, &success_block)
124
- ensure_utf_8 :name, name
125
- ensure_supported_payload data
139
+ def publish(name, data = nil, &success_block)
140
+ raise Ably::Exceptions::ChannelInactive.new('Cannot publish messages on a detached channel') if detached? || detaching?
141
+ raise Ably::Exceptions::ChannelInactive.new('Cannot publish messages on a failed channel') if failed?
126
142
 
127
- create_message(name, data).tap do |message|
128
- message.callback(&success_block) if block_given?
129
- queue_message message
143
+ if !connection.can_publish_messages?
144
+ raise Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. Client is configured to disallow queueing of messages and connection is currently #{connection.state}")
145
+ end
146
+
147
+ messages = if name.kind_of?(Enumerable)
148
+ name
149
+ else
150
+ ensure_utf_8 :name, name, allow_nil: true
151
+ ensure_supported_payload data
152
+ [{ name: name, data: data }]
153
+ end
154
+
155
+ queue_messages(messages).tap do |deferrable|
156
+ deferrable.callback &success_block if block_given?
130
157
  end
131
158
  end
132
159
 
@@ -163,6 +190,8 @@ module Ably
163
190
  # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callback
164
191
  #
165
192
  def attach(&success_block)
193
+ raise Ably::Exceptions::InvalidStateChange.new("Cannot ATTACH channel when the connection is in a closed, suspended or failed state. Connection state: #{connection.state}") if connection.closing? || connection.closed? || connection.suspended? || connection.failed?
194
+
166
195
  transition_state_machine :attaching if can_transition_to?(:attaching)
167
196
  deferrable_for_state_change_to(STATE.Attached, &success_block)
168
197
  end
@@ -170,10 +199,18 @@ module Ably
170
199
  # Detach this channel, and call the block if provided when in a Detached or Failed state
171
200
  #
172
201
  # @yield [Ably::Realtime::Channel] Block is called as soon as this channel is in the Detached or Failed state
173
- # @return [void]
202
+ # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callback
174
203
  #
175
204
  def detach(&success_block)
176
- raise exception_for_state_change_to(:detaching) if failed? || initialized?
205
+ if initialized?
206
+ success_block.call if block_given?
207
+ return Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
208
+ EventMachine.next_tick { deferrable.succeed }
209
+ end
210
+ end
211
+
212
+ raise exception_for_state_change_to(:detaching) if failed?
213
+
177
214
  transition_state_machine :detaching if can_transition_to?(:detaching)
178
215
  deferrable_for_state_change_to(STATE.Detached, &success_block)
179
216
  end
@@ -237,6 +274,11 @@ module Ably
237
274
  @attached_serial = serial
238
275
  end
239
276
 
277
+ # @api private
278
+ def update_options(channel_options)
279
+ @options = channel_options.clone.freeze
280
+ end
281
+
240
282
  # Used by {Ably::Modules::StateEmitter} to debug state changes
241
283
  # @api private
242
284
  def logger
@@ -257,16 +299,50 @@ module Ably
257
299
  end
258
300
  end
259
301
 
260
- # Queue message and process queue if channel is attached.
302
+ # Queue messages and process queue if channel is attached.
261
303
  # If channel is not yet attached, attempt to attach it before the message queue is processed.
262
- def queue_message(message)
263
- queue << message
304
+ # @returns [Ably::Util::SafeDeferrable]
305
+ def queue_messages(raw_messages)
306
+ messages = Array(raw_messages).map { |msg| create_message(msg) }
307
+ queue.push *messages
264
308
 
265
309
  if attached?
266
310
  process_queue
267
311
  else
268
312
  attach
269
313
  end
314
+
315
+ if messages.count == 1
316
+ # A message is a Deferrable so, if publishing only one message, simply return that Deferrable
317
+ messages.first
318
+ else
319
+ deferrable_for_multiple_messages(messages)
320
+ end
321
+ end
322
+
323
+ # A deferrable object that calls the success callback once all messages are delivered
324
+ # If any message fails, the errback is called immediately
325
+ # Only one callback or errback is ever called i.e. if a group of messages all fail, only once
326
+ # errback will be invoked
327
+ def deferrable_for_multiple_messages(messages)
328
+ expected_deliveries = messages.count
329
+ actual_deliveries = 0
330
+ failed = false
331
+
332
+ Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
333
+ messages.each do |message|
334
+ message.callback do
335
+ return if failed
336
+ actual_deliveries += 1
337
+ deferrable.succeed messages if actual_deliveries == expected_deliveries
338
+ end
339
+ message.errback do |error|
340
+ return if failed
341
+ failed = true
342
+ deferrable.fail error, message
343
+ end
344
+ end
345
+ end
270
346
  end
271
347
 
272
348
  def messages_in_queue?
@@ -282,19 +358,15 @@ module Ably
282
358
  end
283
359
 
284
360
  def send_messages_within_protocol_message(messages)
285
- client.connection.send_protocol_message(
361
+ connection.send_protocol_message(
286
362
  action: Ably::Models::ProtocolMessage::ACTION.Message.to_i,
287
363
  channel: name,
288
364
  messages: messages
289
365
  )
290
366
  end
291
367
 
292
- def create_message(name, data)
293
- message = { name: name }
294
- message.merge!(data: data) unless data.nil?
295
- message.merge!(clientId: client.client_id) if client.client_id
296
-
297
- Ably::Models::Message.new(message, logger: logger).tap do |message|
368
+ def create_message(message)
369
+ Ably::Models::Message(message.dup).tap do |message|
298
370
  message.encode self
299
371
  end
300
372
  end