ably 1.0.7 → 1.1.4.rc

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +14 -0
  3. data/.travis.yml +10 -8
  4. data/CHANGELOG.md +58 -4
  5. data/LICENSE +1 -3
  6. data/README.md +9 -5
  7. data/Rakefile +32 -0
  8. data/SPEC.md +920 -565
  9. data/ably.gemspec +16 -11
  10. data/lib/ably/auth.rb +28 -2
  11. data/lib/ably/exceptions.rb +10 -4
  12. data/lib/ably/logger.rb +7 -1
  13. data/lib/ably/models/channel_state_change.rb +1 -1
  14. data/lib/ably/models/connection_state_change.rb +1 -1
  15. data/lib/ably/models/device_details.rb +87 -0
  16. data/lib/ably/models/device_push_details.rb +86 -0
  17. data/lib/ably/models/error_info.rb +23 -2
  18. data/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
  19. data/lib/ably/models/protocol_message.rb +32 -2
  20. data/lib/ably/models/push_channel_subscription.rb +89 -0
  21. data/lib/ably/modules/conversions.rb +1 -1
  22. data/lib/ably/modules/encodeable.rb +1 -1
  23. data/lib/ably/modules/exception_codes.rb +128 -0
  24. data/lib/ably/modules/model_common.rb +15 -2
  25. data/lib/ably/modules/state_machine.rb +2 -2
  26. data/lib/ably/realtime.rb +1 -0
  27. data/lib/ably/realtime/auth.rb +1 -1
  28. data/lib/ably/realtime/channel.rb +24 -102
  29. data/lib/ably/realtime/channel/channel_manager.rb +2 -6
  30. data/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  31. data/lib/ably/realtime/channel/publisher.rb +74 -0
  32. data/lib/ably/realtime/channel/push_channel.rb +62 -0
  33. data/lib/ably/realtime/client.rb +91 -3
  34. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
  35. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  36. data/lib/ably/realtime/connection.rb +34 -20
  37. data/lib/ably/realtime/connection/connection_manager.rb +25 -9
  38. data/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  39. data/lib/ably/realtime/presence.rb +4 -4
  40. data/lib/ably/realtime/presence/members_map.rb +3 -3
  41. data/lib/ably/realtime/push.rb +40 -0
  42. data/lib/ably/realtime/push/admin.rb +61 -0
  43. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  44. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  45. data/lib/ably/rest.rb +1 -0
  46. data/lib/ably/rest/channel.rb +53 -17
  47. data/lib/ably/rest/channel/push_channel.rb +62 -0
  48. data/lib/ably/rest/client.rb +161 -35
  49. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +4 -1
  50. data/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  51. data/lib/ably/rest/presence.rb +1 -0
  52. data/lib/ably/rest/push.rb +42 -0
  53. data/lib/ably/rest/push/admin.rb +54 -0
  54. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  55. data/lib/ably/rest/push/device_registrations.rb +103 -0
  56. data/lib/ably/version.rb +7 -2
  57. data/spec/acceptance/realtime/auth_spec.rb +22 -21
  58. data/spec/acceptance/realtime/channel_history_spec.rb +26 -20
  59. data/spec/acceptance/realtime/channel_spec.rb +177 -59
  60. data/spec/acceptance/realtime/client_spec.rb +153 -0
  61. data/spec/acceptance/realtime/connection_failures_spec.rb +72 -6
  62. data/spec/acceptance/realtime/connection_spec.rb +129 -18
  63. data/spec/acceptance/realtime/message_spec.rb +36 -34
  64. data/spec/acceptance/realtime/presence_spec.rb +201 -167
  65. data/spec/acceptance/realtime/push_admin_spec.rb +736 -0
  66. data/spec/acceptance/realtime/push_spec.rb +27 -0
  67. data/spec/acceptance/rest/auth_spec.rb +4 -3
  68. data/spec/acceptance/rest/base_spec.rb +2 -2
  69. data/spec/acceptance/rest/channel_spec.rb +79 -4
  70. data/spec/acceptance/rest/channels_spec.rb +6 -0
  71. data/spec/acceptance/rest/client_spec.rb +129 -10
  72. data/spec/acceptance/rest/message_spec.rb +158 -6
  73. data/spec/acceptance/rest/push_admin_spec.rb +952 -0
  74. data/spec/acceptance/rest/push_spec.rb +25 -0
  75. data/spec/acceptance/rest/time_spec.rb +1 -1
  76. data/spec/run_parallel_tests +33 -0
  77. data/spec/spec_helper.rb +1 -1
  78. data/spec/support/debug_failure_helper.rb +9 -5
  79. data/spec/support/test_app.rb +2 -2
  80. data/spec/unit/logger_spec.rb +10 -3
  81. data/spec/unit/models/device_details_spec.rb +102 -0
  82. data/spec/unit/models/device_push_details_spec.rb +101 -0
  83. data/spec/unit/models/error_info_spec.rb +51 -3
  84. data/spec/unit/models/message_spec.rb +17 -2
  85. data/spec/unit/models/presence_message_spec.rb +1 -1
  86. data/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  87. data/spec/unit/modules/enum_spec.rb +1 -1
  88. data/spec/unit/realtime/client_spec.rb +13 -1
  89. data/spec/unit/realtime/connection_spec.rb +1 -1
  90. data/spec/unit/realtime/push_channel_spec.rb +36 -0
  91. data/spec/unit/rest/channel_spec.rb +8 -1
  92. data/spec/unit/rest/client_spec.rb +30 -0
  93. data/spec/unit/rest/push_channel_spec.rb +36 -0
  94. metadata +94 -31
@@ -43,25 +43,38 @@ module Ably::Realtime
43
43
  end
44
44
 
45
45
  unless client.auth.authentication_security_requirements_met?
46
- connection.transition_state_machine :failed, reason: Ably::Exceptions::InsecureRequest.new('Cannot use Basic Auth over non-TLS connections', 401, 40103)
46
+ connection.transition_state_machine :failed, reason: Ably::Exceptions::InsecureRequest.new('Cannot use Basic Auth over non-TLS connections', 401, Ably::Exceptions::Codes::INVALID_USE_OF_BASIC_AUTH_OVER_NONTLS_TRANSPORT)
47
47
  return
48
48
  end
49
49
 
50
50
  logger.debug { 'ConnectionManager: Opening a websocket transport connection' }
51
51
 
52
+ # The socket attempt can fail at the same time as a timer firing so ensure
53
+ # only one outcome is processed from this setup attempt
54
+ setup_attempt_status = {}
55
+ setup_failed = lambda do
56
+ return true if setup_attempt_status[:failed]
57
+ setup_attempt_status[:failed] = true
58
+ false
59
+ end
60
+
52
61
  connection.create_websocket_transport.tap do |socket_deferrable|
53
62
  socket_deferrable.callback do |websocket_transport|
54
63
  subscribe_to_transport_events websocket_transport
55
64
  yield websocket_transport if block_given?
56
65
  end
57
66
  socket_deferrable.errback do |error|
67
+ next if setup_failed.call
58
68
  connection_opening_failed error
59
69
  end
60
70
  end
61
71
 
72
+ # The connection request timeout must be marginally higher than the REST request timeout to ensure
73
+ # any HTTP auth request failure due to timeout triggers before the connection timer kicks in
62
74
  logger.debug { "ConnectionManager: Setting up automatic connection timeout timer for #{realtime_request_timeout}s" }
63
75
  create_timeout_timer_whilst_in_state(:connecting, realtime_request_timeout) do
64
- connection_opening_failed Ably::Exceptions::ConnectionTimeout.new("Connection to Ably timed out after #{realtime_request_timeout}s", nil, 80014)
76
+ next if setup_failed.call
77
+ connection_opening_failed Ably::Exceptions::ConnectionTimeout.new("Connection to Ably timed out after #{realtime_request_timeout}s", nil, Ably::Exceptions::Codes::CONNECTION_TIMED_OUT)
65
78
  end
66
79
  end
67
80
 
@@ -72,7 +85,7 @@ module Ably::Realtime
72
85
  if error.kind_of?(Ably::Exceptions::BaseAblyException)
73
86
  # Authentication errors that indicate the authentication failure is terminal should move to the failed state
74
87
  if ([401, 403].include?(error.status) && !RESOLVABLE_ERROR_CODES.fetch(:token_expired).include?(error.code)) ||
75
- (error.code == Ably::Exceptions::INVALID_CLIENT_ID)
88
+ (error.code == Ably::Exceptions::Codes::INVALID_CLIENT_ID)
76
89
  connection.transition_state_machine :failed, reason: error
77
90
  return
78
91
  end
@@ -80,7 +93,12 @@ module Ably::Realtime
80
93
 
81
94
  logger.warn { "ConnectionManager: Connection to #{connection.current_host}:#{connection.port} failed; #{error.message}" }
82
95
  next_state = get_next_retry_state_info
83
- connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, 80000, error)
96
+
97
+ if connection.state == next_state.fetch(:state)
98
+ logger.error { "ConnectionManager: Skipping next retry state after connection opening failed as already in state #{next_state}\n#{caller[0..20].join("\n")}" }
99
+ else
100
+ connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: Ably::Exceptions::ConnectionError.new("Connection failed: #{error.message}", nil, Ably::Exceptions::Codes::CONNECTION_FAILED, error)
101
+ end
84
102
  end
85
103
 
86
104
  # Called whenever a new connection is made
@@ -100,13 +118,11 @@ module Ably::Realtime
100
118
  resend_pending_message_ack_queue
101
119
  else
102
120
  logger.debug { "ConnectionManager: Connection was not resumed, old connection ID #{connection.id} has been updated with new connection ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}" }
103
- connection.reset_client_serial
104
121
  nack_messages_on_all_channels protocol_message.error
105
122
  force_reattach_on_channels protocol_message.error
106
123
  end
107
124
  else
108
125
  logger.debug { "ConnectionManager: New connection created with ID #{protocol_message.connection_id} and key #{protocol_message.connection_key}" }
109
- connection.reset_client_serial
110
126
  end
111
127
 
112
128
  reattach_suspended_channels protocol_message.error
@@ -319,7 +335,7 @@ module Ably::Realtime
319
335
  @liveness_timer = EventMachine::Timer.new(connection.heartbeat_interval + 0.1) do
320
336
  if connection.connected? && (connection.time_since_connection_confirmed_alive? >= connection.heartbeat_interval)
321
337
  msg = "No activity seen from realtime in #{connection.heartbeat_interval}; assuming connection has dropped";
322
- error = Ably::Exceptions::ConnectionTimeout.new(msg, 80003, 408)
338
+ error = Ably::Exceptions::ConnectionTimeout.new(msg, Ably::Exceptions::Codes::DISCONNECTED, 408)
323
339
  connection.transition_state_machine! :disconnected, reason: error
324
340
  end
325
341
  end
@@ -492,9 +508,9 @@ module Ably::Realtime
492
508
  connection.transition_state_machine :closed
493
509
  elsif !connection.closed? && !connection.disconnected? && !connection.failed? && !connection.suspended?
494
510
  exception = if reason
495
- Ably::Exceptions::TransportClosed.new(reason, nil, 80003)
511
+ Ably::Exceptions::TransportClosed.new(reason, nil, Ably::Exceptions::Codes::DISCONNECTED)
496
512
  else
497
- Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, 80003)
513
+ Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, Ably::Exceptions::Codes::DISCONNECTED)
498
514
  end
499
515
  next_state = get_next_retry_state_info
500
516
  connection.transition_state_machine next_state.fetch(:state), retry_in: next_state.fetch(:pause), reason: exception
@@ -157,7 +157,7 @@ module Ably::Realtime
157
157
  logger.debug { "WebsocketTransport: Prot msg recv <=: #{action_name} - #{event_data}" }
158
158
 
159
159
  if protocol_message.invalid?
160
- error = Ably::Exceptions::ProtocolError.new("Invalid Protocol Message received: #{event_data}\nConnection moving to the failed state as the protocol is invalid and unsupported", 400, 80013)
160
+ error = Ably::Exceptions::ProtocolError.new("Invalid Protocol Message received: #{event_data}\nConnection moving to the failed state as the protocol is invalid and unsupported", 400, Ably::Exceptions::Codes::PROTOCOL_ERROR)
161
161
  logger.fatal { "WebsocketTransport: #{error.message}" }
162
162
  failed_protocol_message = Ably::Models::ProtocolMessage.new(
163
163
  action: Ably::Models::ProtocolMessage::ACTION.Error,
@@ -233,7 +233,7 @@ module Ably::Realtime
233
233
  deferrable.fail Ably::Exceptions::InvalidState.new(
234
234
  'Presence state is out of sync as channel is SUSPENDED. Presence#get on a SUSPENDED channel is only supported with option wait_for_sync: false',
235
235
  nil,
236
- 91005
236
+ Ably::Exceptions::Codes::PRESENCE_STATE_IS_OUT_OF_SYNC
237
237
  )
238
238
  end
239
239
  return deferrable
@@ -330,7 +330,7 @@ module Ably::Realtime
330
330
  def send_presence_protocol_message(presence_action, client_id, data)
331
331
  presence_message = create_presence_message(presence_action, client_id, data)
332
332
  unless presence_message.client_id
333
- raise Ably::Exceptions::Standard.new('Unable to enter create presence message without a client_id', 400, 91000)
333
+ raise Ably::Exceptions::Standard.new('Unable to enter create presence message without a client_id', 400, Ably::Exceptions::Codes::UNABLE_TO_ENTER_PRESENCE_CHANNEL_NO_CLIENTID)
334
334
  end
335
335
 
336
336
  protocol_message = {
@@ -437,13 +437,13 @@ module Ably::Realtime
437
437
 
438
438
  def attach_channel_then(deferrable)
439
439
  if channel.detached? || channel.failed?
440
- deferrable.fail Ably::Exceptions::InvalidState.new("Operation is not allowed when channel is in #{channel.state}", 400, 91001)
440
+ deferrable.fail Ably::Exceptions::InvalidState.new("Operation is not allowed when channel is in #{channel.state}", 400, Ably::Exceptions::Codes::UNABLE_TO_ENTER_PRESENCE_CHANNEL_INVALID_CHANNEL_STATE)
441
441
  else
442
442
  channel.unsafe_once(:attached, :detached, :failed) do |channel_state_change|
443
443
  if channel_state_change.current == :attached
444
444
  yield
445
445
  else
446
- deferrable.fail Ably::Exceptions::InvalidState.new("Operation failed as channel transitioned to #{channel_state_change.current}", 400, 91001)
446
+ deferrable.fail Ably::Exceptions::InvalidState.new("Operation failed as channel transitioned to #{channel_state_change.current}", 400, Ably::Exceptions::Codes::UNABLE_TO_ENTER_PRESENCE_CHANNEL_INVALID_CHANNEL_STATE)
447
447
  end
448
448
  end
449
449
  channel.attach
@@ -275,7 +275,7 @@ module Ably::Realtime
275
275
  presence_message_client_id = presence_message.client_id || client.auth.client_id
276
276
  re_enter_error = Ably::Models::ErrorInfo.new(
277
277
  message: "unable to automatically re-enter presence channel for client_id '#{presence_message_client_id}'. Source error code #{error.code} and message '#{error.message}'",
278
- code: 91004
278
+ code: Ably::Exceptions::Codes::UNABLE_TO_AUTOMATICALLY_REENTER_PRESENCE_CHANNEL
279
279
  )
280
280
  channel.emit :update, Ably::Models::ChannelStateChange.new(
281
281
  current: channel.state,
@@ -312,14 +312,14 @@ module Ably::Realtime
312
312
  when Ably::Models::PresenceMessage::ACTION.Leave
313
313
  remove_presence_member presence_message
314
314
  else
315
- Ably::Exceptions::ProtocolError.new("Protocol error, unknown presence action #{presence_message.action}", 400, 80013)
315
+ Ably::Exceptions::ProtocolError.new("Protocol error, unknown presence action #{presence_message.action}", 400, Ably::Exceptions::Codes::PROTOCOL_ERROR)
316
316
  end
317
317
  end
318
318
 
319
319
  def ensure_presence_message_is_valid(presence_message)
320
320
  return true if presence_message.connection_id
321
321
 
322
- error = Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing connectionId", 400, 80013)
322
+ error = Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing connectionId", 400, Ably::Exceptions::Codes::PROTOCOL_ERROR)
323
323
  logger.error { "PresenceMap: On channel '#{channel.name}' error: #{error}" }
324
324
  end
325
325
 
@@ -0,0 +1,40 @@
1
+ require 'ably/realtime/push/admin'
2
+
3
+ module Ably
4
+ module Realtime
5
+ # Class providing push notification functionality
6
+ class Push
7
+ # @private
8
+ attr_reader :client
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ # Admin features for push notifications like managing devices and channel subscriptions
15
+ # @return [Ably::Realtime::Push::Admin]
16
+ def admin
17
+ @admin ||= Admin.new(self)
18
+ end
19
+
20
+ # Activate this device for push notifications by registering with the push transport such as GCM/APNS
21
+ #
22
+ # @note This is unsupported in the Ruby library
23
+ def activate(*arg)
24
+ raise_unsupported
25
+ end
26
+
27
+ # Deactivate this device for push notifications by removing the registration with the push transport such as GCM/APNS
28
+ #
29
+ # @note This is unsupported in the Ruby library
30
+ def deactivate(*arg)
31
+ raise_unsupported
32
+ end
33
+
34
+ private
35
+ def raise_unsupported
36
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. All PushChannel methods are unavailable'
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,61 @@
1
+ require 'ably/realtime/push/device_registrations'
2
+ require 'ably/realtime/push/channel_subscriptions'
3
+
4
+ module Ably::Realtime
5
+ class Push
6
+ # Class providing push notification administrative functionality
7
+ # for registering devices and attaching to channels etc.
8
+ class Admin
9
+ include Ably::Modules::AsyncWrapper
10
+ include Ably::Modules::Conversions
11
+
12
+ # @api private
13
+ attr_reader :client
14
+
15
+ # @api private
16
+ attr_reader :push
17
+
18
+ def initialize(push)
19
+ @push = push
20
+ @client = push.client
21
+ end
22
+
23
+ # (see Ably::Rest::Push#publish)
24
+ #
25
+ # @yield Block is invoked upon successful publish of the message
26
+ # @return [Ably::Util::SafeDeferrable]
27
+ #
28
+ def publish(recipient, data, &callback)
29
+ raise ArgumentError, "Expecting a Hash object for recipient, got #{recipient.class}" unless recipient.kind_of?(Hash)
30
+ raise ArgumentError, "Recipient data is empty. You must provide recipient details" if recipient.empty?
31
+ raise ArgumentError, "Expecting a Hash object for data, got #{data.class}" unless data.kind_of?(Hash)
32
+ raise ArgumentError, "Push data field is empty. You must provide attributes for the push notification" if data.empty?
33
+
34
+ async_wrap(callback) do
35
+ rest_push_admin.publish(recipient, data)
36
+ end
37
+ end
38
+
39
+ # Manage device registrations
40
+ # @return [Ably::Realtime::Push::DeviceRegistrations]
41
+ def device_registrations
42
+ @device_registrations ||= DeviceRegistrations.new(self)
43
+ end
44
+
45
+ # Manage channel subscriptions for devices or clients
46
+ # @return [Ably::Realtime::Push::ChannelSubscriptions]
47
+ def channel_subscriptions
48
+ @channel_subscriptions ||= ChannelSubscriptions.new(self)
49
+ end
50
+
51
+ private
52
+ def rest_push_admin
53
+ client.rest_client.push.admin
54
+ end
55
+
56
+ def logger
57
+ client.logger
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,108 @@
1
+ module Ably::Realtime
2
+ class Push
3
+ # Manage push notification channel subscriptions for devices or clients
4
+ class ChannelSubscriptions
5
+ include Ably::Modules::Conversions
6
+ include Ably::Modules::AsyncWrapper
7
+
8
+ # @api private
9
+ attr_reader :client
10
+
11
+ # @api private
12
+ attr_reader :admin
13
+
14
+ def initialize(admin)
15
+ @admin = admin
16
+ @client = admin.client
17
+ end
18
+
19
+ # (see Ably::Rest::Push::ChannelSubscriptions#list)
20
+ #
21
+ # @yield Block is invoked when request succeeds
22
+ # @return [Ably::Util::SafeDeferrable]
23
+ #
24
+ def list(params, &callback)
25
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
26
+
27
+ if (IdiomaticRubyWrapper(params).keys & [:channel, :client_id, :device_id]).length == 0
28
+ raise ArgumentError, "at least one channel, client_id or device_id filter param must be provided"
29
+ end
30
+
31
+ async_wrap(callback) do
32
+ rest_channel_subscriptions.list(params.merge(async_blocking_operations: true))
33
+ end
34
+ end
35
+
36
+ # (see Ably::Rest::Push::ChannelSubscriptions#list_channels)
37
+ #
38
+ # @yield Block is invoked when request succeeds
39
+ # @return [Ably::Util::SafeDeferrable]
40
+ #
41
+ def list_channels(params = {}, &callback)
42
+ params = {} if params.nil?
43
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
44
+
45
+ async_wrap(callback) do
46
+ rest_channel_subscriptions.list_channels(params.merge(async_blocking_operations: true))
47
+ end
48
+ end
49
+
50
+ # (see Ably::Rest::Push::ChannelSubscriptions#save)
51
+ #
52
+ # @yield Block is invoked when request succeeds
53
+ # @return [Ably::Util::SafeDeferrable]
54
+ #
55
+ def save(push_channel_subscription, &callback)
56
+ push_channel_subscription_object = PushChannelSubscription(push_channel_subscription)
57
+ raise ArgumentError, "Channel is required yet is empty" if push_channel_subscription_object.channel.to_s.empty?
58
+
59
+ async_wrap(callback) do
60
+ rest_channel_subscriptions.save(push_channel_subscription)
61
+ end
62
+ end
63
+
64
+ # (see Ably::Rest::Push::ChannelSubscriptions#remove)
65
+ #
66
+ # @yield Block is invoked when request succeeds
67
+ # @return [Ably::Util::SafeDeferrable]
68
+ #
69
+ def remove(push_channel_subscription, &callback)
70
+ push_channel_subscription_object = PushChannelSubscription(push_channel_subscription)
71
+ raise ArgumentError, "Channel is required yet is empty" if push_channel_subscription_object.channel.to_s.empty?
72
+ if push_channel_subscription_object.client_id.to_s.empty? && push_channel_subscription_object.device_id.to_s.empty?
73
+ raise ArgumentError, "Either client_id or device_id must be present"
74
+ end
75
+
76
+ async_wrap(callback) do
77
+ rest_channel_subscriptions.remove(push_channel_subscription)
78
+ end
79
+ end
80
+
81
+ # (see Ably::Rest::Push::ChannelSubscriptions#remove_where)
82
+ #
83
+ # @yield Block is invoked when request succeeds
84
+ # @return [Ably::Util::SafeDeferrable]
85
+ #
86
+ def remove_where(params, &callback)
87
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
88
+
89
+ if (IdiomaticRubyWrapper(params).keys & [:channel, :client_id, :device_id]).length == 0
90
+ raise ArgumentError, "at least one channel, client_id or device_id filter param must be provided"
91
+ end
92
+
93
+ async_wrap(callback) do
94
+ rest_channel_subscriptions.remove_where(params)
95
+ end
96
+ end
97
+
98
+ private
99
+ def rest_channel_subscriptions
100
+ client.rest_client.push.admin.channel_subscriptions
101
+ end
102
+
103
+ def logger
104
+ client.logger
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,105 @@
1
+ module Ably::Realtime
2
+ class Push
3
+ # Manage device registrations for push notifications
4
+ class DeviceRegistrations
5
+ include Ably::Modules::Conversions
6
+ include Ably::Modules::AsyncWrapper
7
+
8
+ # @api private
9
+ attr_reader :client
10
+
11
+ # @api private
12
+ attr_reader :admin
13
+
14
+ def initialize(admin)
15
+ @admin = admin
16
+ @client = admin.client
17
+ end
18
+
19
+ # (see Ably::Rest::Push::DeviceRegistrations#get)
20
+ #
21
+ # @yield Block is invoked when request succeeds
22
+ # @return [Ably::Util::SafeDeferrable]
23
+ #
24
+ def get(device_id, &callback)
25
+ device_id = device_id.id if device_id.kind_of?(Ably::Models::DeviceDetails)
26
+ raise ArgumentError, "device_id must be a string or DeviceDetails object" unless device_id.kind_of?(String)
27
+
28
+ async_wrap(callback) do
29
+ rest_device_registrations.get(device_id)
30
+ end
31
+ end
32
+
33
+ # (see Ably::Rest::Push::DeviceRegistrations#list)
34
+ #
35
+ # @yield Block is invoked when request succeeds
36
+ # @return [Ably::Util::SafeDeferrable]
37
+ #
38
+ def list(params = {}, &callback)
39
+ params = {} if params.nil?
40
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
41
+ raise ArgumentError, "device_id filter cannot be specified alongside a client_id filter. Use one or the other" if params[:client_id] && params[:device_id]
42
+
43
+ async_wrap(callback) do
44
+ rest_device_registrations.list(params.merge(async_blocking_operations: true))
45
+ end
46
+ end
47
+
48
+ # (see Ably::Rest::Push::DeviceRegistrations#save)
49
+ #
50
+ # @yield Block is invoked when request succeeds
51
+ # @return [Ably::Util::SafeDeferrable]
52
+ #
53
+ def save(device, &callback)
54
+ device_details = DeviceDetails(device)
55
+ raise ArgumentError, "Device ID is required yet is empty" if device_details.id.nil? || device_details == ''
56
+
57
+ async_wrap(callback) do
58
+ rest_device_registrations.save(device_details)
59
+ end
60
+ end
61
+
62
+ # (see Ably::Rest::Push::DeviceRegistrations#remove)
63
+ #
64
+ # @yield Block is invoked when request succeeds
65
+ # @return [Ably::Util::SafeDeferrable]
66
+ #
67
+ def remove(device_id, &callback)
68
+ device_id = device_id.id if device_id.kind_of?(Ably::Models::DeviceDetails)
69
+ raise ArgumentError, "device_id must be a string or DeviceDetails object" unless device_id.kind_of?(String)
70
+
71
+ async_wrap(callback) do
72
+ rest_device_registrations.remove(device_id)
73
+ end
74
+ end
75
+
76
+ # (see Ably::Rest::Push::DeviceRegistrations#remove_where)
77
+ #
78
+ # @yield Block is invoked when request succeeds
79
+ # @return [Ably::Util::SafeDeferrable]
80
+ #
81
+ def remove_where(params = {}, &callback)
82
+ filter = if params.kind_of?(Ably::Models::DeviceDetails)
83
+ { 'deviceId' => params.id }
84
+ else
85
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
86
+ raise ArgumentError, "device_id filter cannot be specified alongside a client_id filter. Use one or the other" if params[:client_id] && params[:device_id]
87
+ IdiomaticRubyWrapper(params).as_json
88
+ end
89
+
90
+ async_wrap(callback) do
91
+ rest_device_registrations.remove_where(filter)
92
+ end
93
+ end
94
+
95
+ private
96
+ def rest_device_registrations
97
+ client.rest_client.push.admin.device_registrations
98
+ end
99
+
100
+ def logger
101
+ client.logger
102
+ end
103
+ end
104
+ end
105
+ end