ably 0.8.14 → 0.8.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -32,6 +32,11 @@ module Ably::Models
32
32
  @make_async = options.fetch(:async_blocking_operations, false)
33
33
 
34
34
  @items = http_response.body
35
+ if @items.nil? || @items.to_s.strip.empty?
36
+ @items = []
37
+ end
38
+ @items = [@items] if @items.kind_of?(Hash)
39
+
35
40
  @items = coerce_items_into(items, @coerce_into) if @coerce_into
36
41
  @items = items.map { |item| yield item } if block_given?
37
42
  end
@@ -50,18 +50,14 @@ module Ably
50
50
  #
51
51
  # @return [void]
52
52
  def on(*event_names, &block)
53
- event_names.each do |event_name|
54
- callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block)
55
- end
53
+ add_callback event_names, proc_for_block(block)
56
54
  end
57
55
 
58
56
  # Equivalent of {#on} but any exception raised in a block will bubble up and cause this client library to fail.
59
57
  # This method should only be used internally by the client library.
60
58
  # @api private
61
59
  def unsafe_on(*event_names, &block)
62
- event_names.each do |event_name|
63
- callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, unsafe: true)
64
- end
60
+ add_callback event_names, proc_for_block(block, unsafe: true)
65
61
  end
66
62
 
67
63
  # On receiving an event maching the event_name, call the provided block only once and remove the registered callback
@@ -70,24 +66,20 @@ module Ably
70
66
  #
71
67
  # @return [void]
72
68
  def once(*event_names, &block)
73
- event_names.each do |event_name|
74
- callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, delete_once_run: true)
75
- end
69
+ add_callback event_names, proc_for_block(block, delete_once_run: true)
76
70
  end
77
71
 
78
72
  # Equivalent of {#once} but any exception raised in a block will bubble up and cause this client library to fail.
79
73
  # This method should only be used internally by the client library.
80
74
  # @api private
81
75
  def unsafe_once(*event_names, &block)
82
- event_names.each do |event_name|
83
- callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, delete_once_run: true, unsafe: true)
84
- end
76
+ add_callback event_names, proc_for_block(block, delete_once_run: true, unsafe: true)
85
77
  end
86
78
 
87
79
  # Emit an event with event_name that will in turn call all matching callbacks setup with `on`
88
80
  def emit(event_name, *args)
89
- callbacks[callbacks_event_coerced(event_name)].
90
- clone.
81
+ [callbacks_any, callbacks[callbacks_event_coerced(event_name)]].each do |callback_arr|
82
+ callback_arr.clone.
91
83
  select do |proc_hash|
92
84
  if proc_hash[:unsafe]
93
85
  proc_hash[:emit_proc].call(*args)
@@ -95,8 +87,9 @@ module Ably
95
87
  safe_yield proc_hash[:emit_proc], *args
96
88
  end
97
89
  end.each do |callback|
98
- callbacks[callbacks_event_coerced(event_name)].delete callback
90
+ callback_arr.delete callback
99
91
  end
92
+ end
100
93
  end
101
94
 
102
95
  # Remove all callbacks for event_name.
@@ -121,6 +114,14 @@ module Ably
121
114
  callbacks[callbacks_event_coerced(event_name)].clear
122
115
  end
123
116
  end
117
+
118
+ if event_names.empty?
119
+ if block_given?
120
+ callbacks_any.delete_if { |proc_hash| proc_hash[:block] == block }
121
+ else
122
+ callbacks_any.clear
123
+ end
124
+ end
124
125
  end
125
126
 
126
127
  private
@@ -128,6 +129,16 @@ module Ably
128
129
  klass.extend ClassMethods
129
130
  end
130
131
 
132
+ def add_callback(event_names, proc_block)
133
+ if event_names.empty?
134
+ callbacks_any << proc_block
135
+ else
136
+ event_names.each do |event_name|
137
+ callbacks[callbacks_event_coerced(event_name)] << proc_block
138
+ end
139
+ end
140
+ end
141
+
131
142
  # Create a Hash with a proc that calls the provided block and returns true if option :delete_once_run is set to true.
132
143
  # #emit automatically deletes any blocks that return true thus allowing a block to be run once
133
144
  def proc_for_block(block, options = {})
@@ -145,6 +156,10 @@ module Ably
145
156
  @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
146
157
  end
147
158
 
159
+ def callbacks_any
160
+ @callbacks_any ||= []
161
+ end
162
+
148
163
  def callbacks_event_coerced(event_name)
149
164
  if self.class.event_emitter_coerce_proc
150
165
  self.class.event_emitter_coerce_proc.call(event_name)
@@ -32,6 +32,14 @@ module Ably::Modules
32
32
  as_json.to_json(*args)
33
33
  end
34
34
 
35
+ # Like to_json but encodes all binary fields to hex
36
+ def to_safe_json(*args)
37
+ as_json.
38
+ each_with_object({}) do |(key, val), obj|
39
+ obj[key] = to_safe_jsonable_val(val)
40
+ end.to_json(*args)
41
+ end
42
+
35
43
  # @!attribute [r] hash
36
44
  # @return [Integer] Compute a hash-code for this hash. Two hashes with the same content will have the same hash code
37
45
  def hash
@@ -39,6 +47,23 @@ module Ably::Modules
39
47
  end
40
48
 
41
49
  private
50
+ def to_safe_jsonable_val(val)
51
+ case val
52
+ when Array
53
+ val.map { |array_val| to_safe_jsonable_val(array_val) }
54
+ when Hash
55
+ val.each_with_object({}) { |(key, hash_val), obj| obj[key] = to_safe_jsonable_val(hash_val) }
56
+ when String
57
+ if val.encoding == Encoding::ASCII_8BIT
58
+ val.unpack("H*").first
59
+ else
60
+ val
61
+ end
62
+ else
63
+ val
64
+ end
65
+ end
66
+
42
67
  def ensure_utf8_string_for(attribute, value)
43
68
  if value
44
69
  raise ArgumentError, "#{attribute} must be a String" unless value.kind_of?(String)
@@ -58,7 +58,7 @@ module Ably::Realtime
58
58
  # all messages awaiting an ACK response should fail immediately
59
59
  def fail_messages_awaiting_ack(error)
60
60
  # Allow a short time for other queued operations to complete before failing all messages
61
- EventMachine.add_timer(0.1) do
61
+ EventMachine.next_tick do
62
62
  error = Ably::Exceptions::MessageDeliveryFailed.new("Channel cannot publish messages whilst state is '#{channel.state}'") unless error
63
63
  fail_messages_in_queue connection.__pending_message_ack_queue__, error
64
64
  fail_messages_in_queue connection.__outgoing_message_queue__, error
@@ -76,10 +76,10 @@ module Ably::Realtime
76
76
 
77
77
  def nack_messages(protocol_message, error)
78
78
  (protocol_message.messages + protocol_message.presence).each do |message|
79
- logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}"
79
+ logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_safe_json}, protocol message: #{protocol_message}"
80
80
  message.fail error
81
81
  end
82
- logger.debug "Calling NACK failure callbacks for #{protocol_message.class.name} - #{protocol_message.to_json}"
82
+ logger.debug "Calling NACK failure callbacks for #{protocol_message.class.name} - #{protocol_message.to_safe_json}"
83
83
  protocol_message.fail error
84
84
  end
85
85
 
@@ -68,6 +68,9 @@ module Ably
68
68
  #
69
69
  # @param (see Ably::Rest::Client#initialize)
70
70
  # @option options (see Ably::Rest::Client#initialize)
71
+ # @option options [Proc] :auth_callback when provided, the Proc will be called with the token params hash as the first argument, whenever a new token is required.
72
+ # Whilst the proc is called synchronously, it does not block the EventMachine reactor as it is run in a separate thread.
73
+ # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent
71
74
  # @option options [Boolean] :queue_messages If false, this disables the default behaviour whereby the library queues messages on a connection in the disconnected or connecting states
72
75
  # @option options [Boolean] :echo_messages If false, prevents messages originating from this connection being echoed back on the same connection
73
76
  # @option options [String] :recover When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery
@@ -144,6 +147,16 @@ module Ably
144
147
  connection.connect(&block)
145
148
  end
146
149
 
150
+ # (see Ably::Rest::Client#request)
151
+ # @yield [Ably::Models::HttpPaginatedResponse<>] An Array of Stats
152
+ #
153
+ # @return [Ably::Util::SafeDeferrable]
154
+ def request(method, path, params = {}, body = nil, headers = {}, &callback)
155
+ async_wrap(callback) do
156
+ rest_client.request(method, path, params, body, headers, async_blocking_operations: true)
157
+ end
158
+ end
159
+
147
160
  # @!attribute [r] endpoint
148
161
  # @return [URI::Generic] Default Ably Realtime endpoint used for all requests
149
162
  def endpoint
@@ -178,14 +178,14 @@ module Ably::Realtime
178
178
 
179
179
  def ack_messages(messages)
180
180
  messages.each do |message|
181
- logger.debug "Calling ACK success callbacks for #{message.class.name} - #{message.to_json}"
181
+ logger.debug "Calling ACK success callbacks for #{message.class.name} - #{message.to_safe_json}"
182
182
  message.succeed message
183
183
  end
184
184
  end
185
185
 
186
186
  def nack_messages(messages, protocol_message)
187
187
  messages.each do |message|
188
- logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}"
188
+ logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_safe_json}, protocol message: #{protocol_message}"
189
189
  message.fail protocol_message.error
190
190
  end
191
191
  end
@@ -200,7 +200,7 @@ module Ably::Realtime
200
200
  return unless ensure_presence_message_is_valid(presence_message)
201
201
 
202
202
  unless should_update_member?(presence_message)
203
- logger.debug "#{self.class.name}: Skipped presence member #{presence_message.action} on channel #{presence.channel.name}.\n#{presence_message.to_json}"
203
+ logger.debug "#{self.class.name}: Skipped presence member #{presence_message.action} on channel #{presence.channel.name}.\n#{presence_message.to_safe_json}"
204
204
  return
205
205
  end
206
206
 
@@ -239,13 +239,13 @@ module Ably::Realtime
239
239
  end
240
240
 
241
241
  def add_presence_member(presence_message)
242
- logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' for event '#{presence_message.action}' #{members.has_key?(presence_message.member_key) ? 'updated' : 'added'}.\n#{presence_message.to_json}"
242
+ logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' for event '#{presence_message.action}' #{members.has_key?(presence_message.member_key) ? 'updated' : 'added'}.\n#{presence_message.to_safe_json}"
243
243
  members[presence_message.member_key] = { present: true, message: presence_message }
244
244
  presence.emit_message presence_message.action, presence_message
245
245
  end
246
246
 
247
247
  def remove_presence_member(presence_message)
248
- logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_json}"
248
+ logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_safe_json}"
249
249
 
250
250
  if in_sync?
251
251
  members.delete presence_message.member_key
@@ -251,15 +251,67 @@ module Ably
251
251
  # Perform an HTTP GET request to the API using configured authentication
252
252
  #
253
253
  # @return [Faraday::Response]
254
+ #
255
+ # @api private
254
256
  def get(path, params = {}, options = {})
255
- request(:get, path, params, options)
257
+ raw_request(:get, path, params, options)
256
258
  end
257
259
 
258
260
  # Perform an HTTP POST request to the API using configured authentication
259
261
  #
260
262
  # @return [Faraday::Response]
263
+ #
264
+ # @api private
261
265
  def post(path, params, options = {})
262
- request(:post, path, params, options)
266
+ raw_request(:post, path, params, options)
267
+ end
268
+
269
+ # Perform an HTTP request to the Ably API
270
+ # This is a convenience for customers who wish to use bleeding edge REST API functionality
271
+ # that is either not documented or is not included in the API for our client libraries.
272
+ # The REST client library provides a function to issue HTTP requests to the Ably endpoints
273
+ # with all the built in functionality of the library such as authentication, paging,
274
+ # fallback hosts, MsgPack and JSON support etc.
275
+ #
276
+ # @param method [Symbol] The HTTP method symbol such as +:get+, +:post+, +:put+
277
+ # @param path [String] The path of the URL such +/channel/foo/publish+
278
+ # @param params [Hash, nil] Optional querystring params
279
+ # @param body [Hash, nil] Optional body for the POST or PUT request, must be nil or a JSON-like object
280
+ # @param headers [Hash, nil] Optional additional headers
281
+ #
282
+ # @return [Ably::Models::HttpPaginatedResponse<>]
283
+ def request(method, path, params = {}, body = nil, headers = {}, options = {})
284
+ raise "Method #{method.to_s.upcase} not supported" unless [:get, :put, :post].include?(method.to_sym)
285
+
286
+ response = case method.to_sym
287
+ when :get
288
+ reauthorise_on_authorisation_failure do
289
+ send_request(method, path, params, headers: headers)
290
+ end
291
+ when :post
292
+ path_with_params = Addressable::URI.new
293
+ path_with_params.query_values = params || {}
294
+ query = path_with_params.query
295
+ reauthorise_on_authorisation_failure do
296
+ send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers)
297
+ end
298
+ end
299
+
300
+ paginated_options = {
301
+ async_blocking_operations: options.delete(:async_blocking_operations),
302
+ }
303
+
304
+ Ably::Models::HttpPaginatedResponse.new(response, path, self, paginated_options)
305
+
306
+ rescue Exceptions::ResourceMissing, Exceptions::ForbiddenRequest, Exceptions::ResourceMissing => e
307
+ response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message)
308
+ Models::HttpPaginatedResponse.new(response, path, self)
309
+ rescue Exceptions::TokenExpired, Exceptions::UnauthorizedRequest => e
310
+ response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message)
311
+ Models::HttpPaginatedResponse.new(response, path, self)
312
+ rescue Exceptions::InvalidRequest, Exceptions::ServerError => e
313
+ response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message)
314
+ Models::HttpPaginatedResponse.new(response, path, self)
263
315
  end
264
316
 
265
317
  # @!attribute [r] endpoint
@@ -360,7 +412,7 @@ module Ably
360
412
  end
361
413
 
362
414
  private
363
- def request(method, path, params = {}, options = {})
415
+ def raw_request(method, path, params = {}, options = {})
364
416
  options = options.clone
365
417
  if options.delete(:disable_automatic_reauthorise) == true
366
418
  send_request(method, path, params, options)
@@ -385,6 +437,11 @@ module Ably
385
437
  connection(use_fallback: use_fallback).send(method, path, params) do |request|
386
438
  unless options[:send_auth_header] == false
387
439
  request.headers[:authorization] = auth.auth_header
440
+ if options[:headers]
441
+ options[:headers].map do |key, val|
442
+ request.headers[key] = val
443
+ end
444
+ end
388
445
  end
389
446
  end
390
447
 
@@ -1,5 +1,5 @@
1
1
  module Ably
2
- VERSION = '0.8.14'
2
+ VERSION = '0.8.15'
3
3
  PROTOCOL_VERSION = '0.8'
4
4
 
5
5
  # Allow a variant to be configured for all instances of this client library
@@ -186,6 +186,35 @@ describe Ably::Realtime::Auth, :event_machine do
186
186
  end
187
187
  end
188
188
 
189
+ context 'with auth_callback blocking' do
190
+ let(:rest_auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key)) }
191
+ let(:client_options) { default_options.merge(auth_callback: auth_callback) }
192
+ let(:pause) { 5 }
193
+
194
+ context 'with a slow auth callback response' do
195
+ let(:auth_callback) do
196
+ Proc.new do
197
+ sleep pause
198
+ rest_auth_client.auth.request_token
199
+ end
200
+ end
201
+
202
+ it 'asynchronously authenticates' do
203
+ timers_called = 0
204
+ block = Proc.new do
205
+ timers_called += 1
206
+ EventMachine.add_timer(0.5, &block)
207
+ end
208
+ block.call
209
+ client.connect
210
+ client.connection.on(:connected) do
211
+ expect(timers_called).to be >= (pause-1) / 0.5
212
+ stop_reactor
213
+ end
214
+ end
215
+ end
216
+ end
217
+
189
218
  context 'when implicitly called, with an explicit ClientOptions client_id' do
190
219
  let(:client_id) { random_str }
191
220
  let(:client_options) { default_options.merge(auth_callback: Proc.new { auth_token_object }, client_id: client_id, log_level: :none) }
@@ -92,12 +92,14 @@ describe Ably::Realtime::Channel, :event_machine do
92
92
 
93
93
  it 'reattaches' do
94
94
  channel.attach do
95
- channel.transition_state_machine :failed, reason: RuntimeError.new
96
- expect(channel).to be_failed
97
- channel.attach do
98
- expect(channel).to be_attached
99
- stop_reactor
95
+ channel.once(:failed) do
96
+ expect(channel).to be_failed
97
+ channel.attach do
98
+ expect(channel).to be_attached
99
+ stop_reactor
100
+ end
100
101
  end
102
+ channel.transition_state_machine :failed, reason: RuntimeError.new
101
103
  end
102
104
  end
103
105
  end
@@ -371,7 +373,7 @@ describe Ably::Realtime::Channel, :event_machine do
371
373
  expect(message_id.uniq.count).to eql(1)
372
374
 
373
375
  # Check that messages use index 0,1,2 in the ID
374
- message_indexes = messages.map { |msg| msg.id.split(':')[1] }
376
+ message_indexes = messages.map { |msg| msg.id.split(':').last }
375
377
  expect(message_indexes).to include("0", "1", "2")
376
378
  stop_reactor
377
379
  end
@@ -229,5 +229,57 @@ describe Ably::Realtime::Client, :event_machine do
229
229
  stop_reactor
230
230
  end
231
231
  end
232
+
233
+ context '#request (#RSC19*)' do
234
+ let(:client_options) { default_options.merge(key: api_key) }
235
+
236
+ context 'get' do
237
+ it 'returns an HttpPaginatedResponse object' do
238
+ subject.request(:get, 'time').callback do |response|
239
+ expect(response).to be_a(Ably::Models::HttpPaginatedResponse)
240
+ expect(response.status_code).to eql(200)
241
+ stop_reactor
242
+ end
243
+ end
244
+
245
+ context '404 request to invalid URL' do
246
+ it 'returns an object with 404 status code and error message' do
247
+ subject.request(:get, 'does-not-exist').callback do |response|
248
+ expect(response).to be_a(Ably::Models::HttpPaginatedResponse)
249
+ expect(response.error_message).to match(/Could not find/)
250
+ expect(response.error_code).to eql(40400)
251
+ expect(response.status_code).to eql(404)
252
+ stop_reactor
253
+ end
254
+ end
255
+ end
256
+
257
+ context 'paged results' do
258
+ let(:channel_name) { random_str }
259
+
260
+ it 'provides paging' do
261
+ 10.times do
262
+ subject.rest_client.request(:post, "/channels/#{channel_name}/publish", {}, { 'name': 'test' })
263
+ end
264
+
265
+ subject.request(:get, "/channels/#{channel_name}/messages", { limit: 2 }).callback do |response|
266
+ expect(response.items.length).to eql(2)
267
+ expect(response).to be_has_next
268
+ response.next do |next_page|
269
+ expect(next_page.items.length).to eql(2)
270
+ expect(next_page).to be_has_next
271
+ first_page_ids = response.items.map { |message| message['id'] }.uniq.sort
272
+ next_page_ids = next_page.items.map { |message| message['id'] }.uniq.sort
273
+ expect(first_page_ids).to_not eql(next_page_ids)
274
+ next_page.next do |third_page|
275
+ expect(third_page.items.length).to eql(2)
276
+ stop_reactor
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
232
284
  end
233
285
  end