ably-rest 0.8.2 → 0.8.3

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