ably-pubsub-core 0.0.1.pre

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 (105) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ably/agent.rb +8 -0
  3. data/lib/ably/auth.rb +791 -0
  4. data/lib/ably/exceptions.rb +169 -0
  5. data/lib/ably/logger.rb +119 -0
  6. data/lib/ably/models/auth_details.rb +45 -0
  7. data/lib/ably/models/channel_details.rb +69 -0
  8. data/lib/ably/models/channel_metrics.rb +107 -0
  9. data/lib/ably/models/channel_occupancy.rb +50 -0
  10. data/lib/ably/models/channel_options.rb +120 -0
  11. data/lib/ably/models/channel_state_change.rb +84 -0
  12. data/lib/ably/models/channel_status.rb +63 -0
  13. data/lib/ably/models/cipher_params.rb +137 -0
  14. data/lib/ably/models/connection_details.rb +149 -0
  15. data/lib/ably/models/connection_state_change.rb +83 -0
  16. data/lib/ably/models/delta_extras.rb +28 -0
  17. data/lib/ably/models/device_details.rb +126 -0
  18. data/lib/ably/models/device_push_details.rb +94 -0
  19. data/lib/ably/models/error_info.rb +104 -0
  20. data/lib/ably/models/http_paginated_response.rb +112 -0
  21. data/lib/ably/models/idiomatic_ruby_wrapper.rb +235 -0
  22. data/lib/ably/models/message.rb +223 -0
  23. data/lib/ably/models/message_encoders/base.rb +135 -0
  24. data/lib/ably/models/message_encoders/base64.rb +40 -0
  25. data/lib/ably/models/message_encoders/cipher.rb +84 -0
  26. data/lib/ably/models/message_encoders/json.rb +34 -0
  27. data/lib/ably/models/message_encoders/utf8.rb +26 -0
  28. data/lib/ably/models/nil_logger.rb +20 -0
  29. data/lib/ably/models/paginated_result.rb +201 -0
  30. data/lib/ably/models/presence_message.rb +244 -0
  31. data/lib/ably/models/protocol_message.rb +288 -0
  32. data/lib/ably/models/push_channel_subscription.rb +114 -0
  33. data/lib/ably/models/stats.rb +242 -0
  34. data/lib/ably/models/stats_types.rb +107 -0
  35. data/lib/ably/models/token_details.rb +148 -0
  36. data/lib/ably/models/token_request.rb +180 -0
  37. data/lib/ably/modules/ably.rb +26 -0
  38. data/lib/ably/modules/async_wrapper.rb +69 -0
  39. data/lib/ably/modules/channels_collection.rb +105 -0
  40. data/lib/ably/modules/conversions.rb +153 -0
  41. data/lib/ably/modules/encodeable.rb +111 -0
  42. data/lib/ably/modules/enum.rb +230 -0
  43. data/lib/ably/modules/event_emitter.rb +200 -0
  44. data/lib/ably/modules/event_machine_helpers.rb +26 -0
  45. data/lib/ably/modules/exception_codes.rb +128 -0
  46. data/lib/ably/modules/http_helpers.rb +41 -0
  47. data/lib/ably/modules/message_emitter.rb +78 -0
  48. data/lib/ably/modules/message_pack.rb +14 -0
  49. data/lib/ably/modules/model_common.rb +84 -0
  50. data/lib/ably/modules/safe_deferrable.rb +83 -0
  51. data/lib/ably/modules/safe_yield.rb +41 -0
  52. data/lib/ably/modules/state_emitter.rb +181 -0
  53. data/lib/ably/modules/state_machine.rb +59 -0
  54. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  55. data/lib/ably/modules/uses_state_machine.rb +111 -0
  56. data/lib/ably/realtime/auth.rb +283 -0
  57. data/lib/ably/realtime/channel/channel_manager.rb +272 -0
  58. data/lib/ably/realtime/channel/channel_properties.rb +40 -0
  59. data/lib/ably/realtime/channel/channel_state_machine.rb +104 -0
  60. data/lib/ably/realtime/channel/publisher.rb +82 -0
  61. data/lib/ably/realtime/channel/push_channel.rb +69 -0
  62. data/lib/ably/realtime/channel.rb +440 -0
  63. data/lib/ably/realtime/channels.rb +71 -0
  64. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +225 -0
  65. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +87 -0
  66. data/lib/ably/realtime/client.rb +363 -0
  67. data/lib/ably/realtime/connection/connection_manager.rb +594 -0
  68. data/lib/ably/realtime/connection/connection_state_machine.rb +144 -0
  69. data/lib/ably/realtime/connection/websocket_transport.rb +287 -0
  70. data/lib/ably/realtime/connection.rb +738 -0
  71. data/lib/ably/realtime/models/nil_channel.rb +30 -0
  72. data/lib/ably/realtime/presence/members_map.rb +395 -0
  73. data/lib/ably/realtime/presence/presence_manager.rb +78 -0
  74. data/lib/ably/realtime/presence/presence_state_machine.rb +57 -0
  75. data/lib/ably/realtime/presence.rb +502 -0
  76. data/lib/ably/realtime/push/admin.rb +75 -0
  77. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  78. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  79. data/lib/ably/realtime/push.rb +25 -0
  80. data/lib/ably/realtime/recovery_key_context.rb +36 -0
  81. data/lib/ably/realtime.rb +60 -0
  82. data/lib/ably/rest/channel/push_channel.rb +59 -0
  83. data/lib/ably/rest/channel.rb +194 -0
  84. data/lib/ably/rest/channels.rb +47 -0
  85. data/lib/ably/rest/client.rb +727 -0
  86. data/lib/ably/rest/middleware/encoder.rb +49 -0
  87. data/lib/ably/rest/middleware/exceptions.rb +53 -0
  88. data/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  89. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +22 -0
  90. data/lib/ably/rest/middleware/logger.rb +58 -0
  91. data/lib/ably/rest/middleware/parse_json.rb +27 -0
  92. data/lib/ably/rest/middleware/parse_message_pack.rb +43 -0
  93. data/lib/ably/rest/presence.rb +113 -0
  94. data/lib/ably/rest/push/admin.rb +58 -0
  95. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  96. data/lib/ably/rest/push/device_registrations.rb +114 -0
  97. data/lib/ably/rest/push.rb +25 -0
  98. data/lib/ably/rest.rb +40 -0
  99. data/lib/ably/util/ably_extensions.rb +29 -0
  100. data/lib/ably/util/crypto.rb +134 -0
  101. data/lib/ably/util/pub_sub.rb +45 -0
  102. data/lib/ably/util/safe_deferrable.rb +44 -0
  103. data/lib/ably/version.rb +7 -0
  104. data/lib/ably.rb +16 -0
  105. metadata +497 -0
@@ -0,0 +1,225 @@
1
+ module Ably::Realtime
2
+ class Client
3
+ # IncomingMessageDispatcher is a (private) class that is used to dispatch {Ably::Models::ProtocolMessage} that are
4
+ # received from Ably via the {Ably::Realtime::Connection}
5
+ class IncomingMessageDispatcher
6
+ ACTION = Ably::Models::ProtocolMessage::ACTION
7
+
8
+ def initialize(client, connection)
9
+ @client = client
10
+ @connection = connection
11
+
12
+ subscribe_to_incoming_protocol_messages
13
+ end
14
+
15
+ private
16
+ def client
17
+ @client
18
+ end
19
+
20
+ def connection
21
+ @connection
22
+ end
23
+
24
+ def channels
25
+ client.channels
26
+ end
27
+
28
+ def get_channel(channel_name)
29
+ channels.fetch(channel_name) do
30
+ logger.warn { "Received channel message for non-existent channel" }
31
+ Ably::Realtime::Models::NilChannel.new
32
+ end
33
+ end
34
+
35
+ def logger
36
+ client.logger
37
+ end
38
+
39
+ def dispatch_protocol_message(*args)
40
+ protocol_message = args.first
41
+
42
+ unless protocol_message.kind_of?(Ably::Models::ProtocolMessage)
43
+ raise ArgumentError, "Expected a ProtocolMessage. Received #{protocol_message}"
44
+ end
45
+
46
+ # RTL15b
47
+ if protocol_message.has_channel_serial? &&
48
+ (
49
+ protocol_message.action == :message ||
50
+ protocol_message.action == :presence ||
51
+ protocol_message.action == :attached
52
+ )
53
+ get_channel(protocol_message.channel).tap do |channel|
54
+ logger.info "Setting channel serial for channel #{channel.name}, " <<
55
+ "Previous: #{channel.properties.channel_serial}, New: #{protocol_message.channel_serial}"
56
+ channel.properties.channel_serial = protocol_message.channel_serial
57
+ end
58
+ end
59
+
60
+ unless protocol_message.action.match_any?(:nack, :error)
61
+ logger.debug { "#{protocol_message.action} received: #{protocol_message}" }
62
+ end
63
+
64
+ connection.set_connection_confirmed_alive
65
+
66
+ case protocol_message.action
67
+ when ACTION.Heartbeat
68
+ when ACTION.Ack
69
+ ack_pending_queue_for_message_serial(protocol_message) if protocol_message.has_message_serial?
70
+
71
+ when ACTION.Nack
72
+ logger.warn { "NACK received: #{protocol_message}" }
73
+ nack_pending_queue_for_message_serial(protocol_message) if protocol_message.has_message_serial?
74
+
75
+ when ACTION.Connect
76
+ when ACTION.Connected
77
+ if connection.closing?
78
+ logger.debug { "Out-of-order incoming CONNECTED ProtocolMessage discarded as connection has moved on and is in state: #{connection.state}" }
79
+ elsif connection.disconnected? || connection.closing? || connection.closed? || connection.failed?
80
+ logger.warn { "Out-of-order incoming CONNECTED ProtocolMessage discarded as connection has moved on and is in state: #{connection.state}" }
81
+ elsif connection.connected?
82
+ logger.debug { "Updated CONNECTED ProtocolMessage received (whilst connected)" }
83
+ process_connected_update_message protocol_message
84
+ connection.set_connection_confirmed_alive # Connection protocol messages can change liveness settings such as max_idle_interval
85
+ else
86
+ process_connected_message protocol_message
87
+ connection.set_connection_confirmed_alive # Connection protocol messages can change liveness settings such as max_idle_interval
88
+ end
89
+
90
+ when ACTION.Disconnect, ACTION.Disconnected
91
+ connection.transition_state_machine :disconnected, reason: protocol_message.error unless connection.disconnected?
92
+
93
+ when ACTION.Close
94
+ when ACTION.Closed
95
+ connection.transition_state_machine :closed unless connection.closed?
96
+
97
+ when ACTION.Error
98
+ if protocol_message.channel
99
+ dispatch_channel_error protocol_message
100
+ else
101
+ process_connection_error protocol_message
102
+ end
103
+
104
+ when ACTION.Attach
105
+ when ACTION.Attached
106
+ get_channel(protocol_message.channel).tap do |channel|
107
+ if channel.attached?
108
+ channel.manager.duplicate_attached_received protocol_message
109
+ else
110
+ if channel.failed?
111
+ logger.warn "Ably::Realtime::Client::IncomingMessageDispatcher - Received an ATTACHED protocol message for FAILED channel #{channel.name}. Ignoring ATTACHED message"
112
+ else
113
+ channel.transition_state_machine :attached, reason: protocol_message.error, resumed: protocol_message.has_channel_resumed_flag?, protocol_message: protocol_message
114
+ end
115
+ end
116
+ end
117
+
118
+ when ACTION.Detach
119
+ when ACTION.Detached
120
+ get_channel(protocol_message.channel).tap do |channel|
121
+ channel.manager.detached_received protocol_message.error
122
+ end
123
+
124
+ when ACTION.Sync
125
+ presence = get_channel(protocol_message.channel).presence
126
+ presence.manager.sync_process_messages protocol_message.channel_serial, protocol_message.presence
127
+
128
+ when ACTION.Presence
129
+ presence = get_channel(protocol_message.channel).presence
130
+ protocol_message.presence.each do |presence_message|
131
+ presence.__incoming_msgbus__.publish :presence, presence_message
132
+ end
133
+
134
+ when ACTION.Message
135
+ channel = get_channel(protocol_message.channel)
136
+ protocol_message.messages.each do |message|
137
+ channel.__incoming_msgbus__.publish :message, message
138
+ end
139
+
140
+ when ACTION.Auth
141
+ client.auth.authorize
142
+
143
+ else
144
+ error = Ably::Exceptions::ProtocolError.new("Protocol Message Action #{protocol_message.action} is unsupported by this MessageDispatcher", 400, Ably::Exceptions::Codes::PROTOCOL_ERROR)
145
+ logger.fatal error.message
146
+ end
147
+ end
148
+
149
+ def dispatch_channel_error(protocol_message)
150
+ logger.warn { "Channel Error message received: #{protocol_message.error}" }
151
+ if !protocol_message.has_message_serial?
152
+ get_channel(protocol_message.channel).transition_state_machine :failed, reason: protocol_message.error
153
+ else
154
+ logger.fatal { "Cannot process ProtocolMessage ERROR with message serial as not yet implemented: #{protocol_message}" }
155
+ end
156
+ end
157
+
158
+ def process_connection_error(protocol_message)
159
+ connection.manager.error_received_from_server(protocol_message.error || Ably::Models::ErrorInfo.new(message: 'Error reason unknown'))
160
+ end
161
+
162
+ def process_connected_message(protocol_message)
163
+ if client.auth.token_client_id_allowed?(protocol_message.connection_details.client_id)
164
+ connection.transition_state_machine :connected, reason: protocol_message.error, protocol_message: protocol_message
165
+ else
166
+ reason = Ably::Exceptions::IncompatibleClientId.new("Client ID '#{protocol_message.connection_details.client_id}' specified by the server is incompatible with the library's configured client ID '#{client.client_id}'")
167
+ connection.transition_state_machine :failed, reason: reason, protocol_message: protocol_message
168
+ end
169
+ end
170
+
171
+ def process_connected_update_message(protocol_message)
172
+ if client.auth.token_client_id_allowed?(protocol_message.connection_details.client_id)
173
+ connection.manager.connected_update protocol_message
174
+ else
175
+ reason = Ably::Exceptions::IncompatibleClientId.new("Client ID '#{protocol_message.connection_details.client_id}' in CONNECTED update specified by the server is incompatible with the library's configured client ID '#{client.client_id}'")
176
+ connection.transition_state_machine :failed, reason: reason, protocol_message: protocol_message
177
+ end
178
+ end
179
+
180
+ def ack_pending_queue_for_message_serial(ack_protocol_message)
181
+ drop_pending_queue_from_ack(ack_protocol_message) do |protocol_message|
182
+ ack_messages protocol_message.messages
183
+ ack_messages protocol_message.presence
184
+ end
185
+ end
186
+
187
+ def nack_pending_queue_for_message_serial(nack_protocol_message)
188
+ drop_pending_queue_from_ack(nack_protocol_message) do |protocol_message|
189
+ nack_messages protocol_message.messages, nack_protocol_message
190
+ nack_messages protocol_message.presence, nack_protocol_message
191
+ end
192
+ end
193
+
194
+ def ack_messages(messages)
195
+ messages.each do |message|
196
+ logger.debug { "Calling ACK success callbacks for #{message.class.name} - #{message.to_json}" }
197
+ message.succeed message
198
+ end
199
+ end
200
+
201
+ def nack_messages(messages, protocol_message)
202
+ messages.each do |message|
203
+ logger.debug { "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}" }
204
+ message.fail protocol_message.error
205
+ end
206
+ end
207
+
208
+ def drop_pending_queue_from_ack(ack_protocol_message)
209
+ message_serial_up_to = ack_protocol_message.message_serial + ack_protocol_message.count - 1
210
+
211
+ while !connection.__pending_message_ack_queue__.empty?
212
+ next_message = connection.__pending_message_ack_queue__.first
213
+ return if next_message.message_serial > message_serial_up_to
214
+ yield connection.__pending_message_ack_queue__.shift
215
+ end
216
+ end
217
+
218
+ def subscribe_to_incoming_protocol_messages
219
+ connection.__incoming_protocol_msgbus__.subscribe(:protocol_message) do |*args|
220
+ dispatch_protocol_message(*args)
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,87 @@
1
+ module Ably::Realtime
2
+ class Client
3
+ # OutgoingMessageDispatcher is a (private) class that is used to deliver
4
+ # outgoing {Ably::Models::ProtocolMessage}s using the {Ably::Realtime::Connection}
5
+ # when the connection state is capable of delivering messages
6
+ class OutgoingMessageDispatcher
7
+ include Ably::Modules::EventMachineHelpers
8
+
9
+ ACTION = Ably::Models::ProtocolMessage::ACTION
10
+
11
+ def initialize(client, connection)
12
+ @client = client
13
+ @connection = connection
14
+
15
+ subscribe_to_outgoing_protocol_message_queue
16
+ setup_event_handlers
17
+ end
18
+
19
+ private
20
+ def client
21
+ @client
22
+ end
23
+
24
+ def connection
25
+ @connection
26
+ end
27
+
28
+ def can_send_messages?
29
+ connection.connected? || connection.closing?
30
+ end
31
+
32
+ def messages_in_outgoing_queue?
33
+ !outgoing_queue.empty?
34
+ end
35
+
36
+ def outgoing_queue
37
+ connection.__outgoing_message_queue__
38
+ end
39
+
40
+ def pending_ack_queue
41
+ connection.__pending_message_ack_queue__
42
+ end
43
+
44
+ def current_transport_outgoing_message_bus
45
+ connection.transport.__outgoing_protocol_msgbus__
46
+ end
47
+
48
+ def deliver_queued_protocol_messages
49
+ condition = -> { can_send_messages? && messages_in_outgoing_queue? }
50
+
51
+ non_blocking_loop_while(condition) do
52
+ protocol_message = outgoing_queue.shift
53
+
54
+ if (!connection.transport)
55
+ protocol_message.fail Ably::Exceptions::TransportClosed.new('Transport disconnected unexpectedly', nil, Ably::Exceptions::Codes::DISCONNECTED)
56
+ next
57
+ end
58
+
59
+ current_transport_outgoing_message_bus.publish :protocol_message, protocol_message
60
+
61
+ if protocol_message.ack_required?
62
+ pending_ack_queue << protocol_message
63
+ else
64
+ protocol_message.succeed protocol_message
65
+ end
66
+ end
67
+ end
68
+
69
+ def subscribe_to_outgoing_protocol_message_queue
70
+ connection.__outgoing_protocol_msgbus__.subscribe(:protocol_message) do |*args|
71
+ deliver_queued_protocol_messages
72
+ end
73
+ end
74
+
75
+ def setup_event_handlers
76
+ connection.unsafe_on(:connected) do
77
+ # Give connection manager enough time to prevent message delivery if necessary
78
+ # For example, if reconnecting and connection and channel state is lost,
79
+ # then the queued messages must be NACK'd
80
+ EventMachine.next_tick do
81
+ deliver_queued_protocol_messages
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,363 @@
1
+ require 'uri'
2
+ require 'ably/realtime/channel/publisher'
3
+ require 'ably/realtime/recovery_key_context'
4
+
5
+ module Ably
6
+ module Realtime
7
+ # A client that extends the functionality of the {Ably::Realtime::Client} and provides additional realtime-specific features.
8
+ #
9
+ class Client
10
+ include Ably::Modules::AsyncWrapper
11
+ include Ably::Realtime::Channel::Publisher
12
+ include Ably::Modules::Conversions
13
+
14
+ extend Forwardable
15
+ using Ably::Util::AblyExtensions
16
+
17
+ DOMAIN = 'realtime.ably.io'
18
+
19
+ # A {Aby::Realtime::Channels} object.
20
+ #
21
+ # @spec RTC3, RTS1
22
+ #
23
+ # @return [Aby::Realtime::Channels]
24
+ #
25
+ attr_reader :channels
26
+
27
+ # An {Ably::Auth} object.
28
+ #
29
+ # @spec RTC4
30
+ #
31
+ # @return [Ably::Auth]
32
+ #
33
+ attr_reader :auth
34
+
35
+ # A {Aby::Realtime::Connection} object.
36
+ #
37
+ # @spec RTC2
38
+ #
39
+ # @return [Aby::Realtime::Connection]
40
+ #
41
+ attr_reader :connection
42
+
43
+ # The {Ably::Rest::Client REST client} instantiated with the same credentials and configuration that is used for all REST operations such as authentication
44
+ # @return [Ably::Rest::Client]
45
+
46
+ # @private
47
+ attr_reader :rest_client
48
+
49
+ # When false the client suppresses messages originating from this connection being echoed back on the same connection. Defaults to true
50
+ # @return [Boolean]
51
+ attr_reader :echo_messages
52
+
53
+ # If false, this disables the default behaviour whereby the library queues messages on a connection in the disconnected or connecting states. Defaults to true
54
+ # @return [Boolean]
55
+ attr_reader :queue_messages
56
+
57
+ # The custom realtime websocket host that is being used if it was provided with the option `:ws_host` when the {Client} was created
58
+ # @return [String,Nil]
59
+ attr_reader :custom_realtime_host
60
+
61
+ # When true, as soon as the client library is instantiated it will connect to Ably. If this attribute is false, a connection must be opened explicitly
62
+ # @return [Boolean]
63
+ attr_reader :auto_connect
64
+
65
+ # 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
66
+ # @return [String,Nil]
67
+ attr_reader :recover
68
+
69
+ # Additional parameters to be sent in the querystring when initiating a realtime connection
70
+ # @return [Hash]
71
+ attr_reader :transport_params
72
+
73
+ def_delegators :auth, :client_id, :auth_options
74
+ def_delegators :@rest_client, :encoders
75
+ def_delegators :@rest_client, :use_tls?, :protocol, :protocol_binary?
76
+ def_delegators :@rest_client, :environment, :custom_host, :custom_port, :custom_tls_port
77
+ def_delegators :@rest_client, :log_level
78
+ def_delegators :@rest_client, :options
79
+
80
+ # Creates a {Ably::Realtime::Client Realtime Client} and configures the {Ably::Auth} object for the connection.
81
+ #
82
+ # @spec RSC1
83
+ #
84
+ # @param (see {Ably::Rest::Client#initialize})
85
+ # @option options (see Ably::Rest::Client#initialize) An options {Hash} object.
86
+ # @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.
87
+ # Whilst the proc is called synchronously, it does not block the EventMachine reactor as it is run in a separate thread.
88
+ # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent
89
+ # @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
90
+ # @option options [Boolean] :echo_messages If false, prevents messages originating from this connection being echoed back on the same connection
91
+ # @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
92
+ # @option options [Boolean] :auto_connect By default as soon as the client library is instantiated it will connect to Ably. You can optionally set this to false and explicitly connect.
93
+ # @option options [Hash] :transport_params Additional parameters to be sent in the querystring when initiating a realtime connection. Keys are Strings, values are Stringifiable(a value must respond to #to_s)
94
+ #
95
+ # @option options [Integer] :channel_retry_timeout (15 seconds). When a channel becomes SUSPENDED, after this delay in seconds, the channel will automatically attempt to reattach if the connection is CONNECTED
96
+ # @option options [Integer] :disconnected_retry_timeout (15 seconds). When the connection enters the DISCONNECTED state, after this delay in seconds, if the state is still DISCONNECTED, the client library will attempt to reconnect automatically
97
+ # @option options [Integer] :suspended_retry_timeout (30 seconds). When the connection enters the SUSPENDED state, after this delay in seconds, if the state is still SUSPENDED, the client library will attempt to reconnect automatically
98
+ # @option options [Boolean] :disable_websocket_heartbeats WebSocket heartbeats are more efficient than protocol level heartbeats, however they can be disabled for development purposes
99
+ #
100
+ # @return [Ably::Realtime::Client]
101
+ #
102
+ # @example
103
+ # # Constructs a {Ably::Realtime::Client} object using an Ably API key or token string.
104
+ # client = Ably::Realtime::Client.new('key.id:secret')
105
+ #
106
+ # # Constructs a {Ably::Realtime::Client} object using an Ably options object.
107
+ # client = Ably::Realtime::Client.new(key: 'key.id:secret', client_id: 'john')
108
+ #
109
+ def initialize(options)
110
+ raise ArgumentError, 'Options Hash is expected' if options.nil?
111
+
112
+ options = options.clone
113
+ if options.kind_of?(String)
114
+ options = if options.match(Ably::Auth::API_KEY_REGEX)
115
+ { key: options }
116
+ else
117
+ { token: options }
118
+ end
119
+ end
120
+
121
+ @transport_params = options.delete(:transport_params).to_h.each_with_object({}) do |(key, value), acc|
122
+ acc[key.to_s] = value.to_s
123
+ end
124
+ @rest_client = Ably::Rest::Client.new(options.merge(realtime_client: self))
125
+ @echo_messages = rest_client.options.fetch_with_default(:echo_messages, true)
126
+ @queue_messages = rest_client.options.fetch_with_default(:queue_messages, true)
127
+ @custom_realtime_host = rest_client.options[:realtime_host] || rest_client.options[:ws_host]
128
+ @auto_connect = rest_client.options.fetch_with_default(:auto_connect, true)
129
+ @recover = rest_client.options.fetch_with_default(:recover, '')
130
+
131
+ @auth = Ably::Realtime::Auth.new(self)
132
+ @channels = Ably::Realtime::Channels.new(self)
133
+ @connection = Ably::Realtime::Connection.new(self, options)
134
+
135
+ unless @recover.nil_or_empty?
136
+ recovery_context = RecoveryKeyContext.from_json(@recover, logger)
137
+ unless recovery_context.nil?
138
+ @channels.set_channel_serials recovery_context.channel_serials # RTN16j
139
+ @connection.set_msg_serial_from_recover = recovery_context.msg_serial # RTN16f
140
+ end
141
+ end
142
+ end
143
+
144
+ # Return a {Ably::Realtime::Channel Realtime Channel} for the given name
145
+ #
146
+ # @param (see Ably::Realtime::Channels#get)
147
+ # @return (see Ably::Realtime::Channels#get)
148
+ #
149
+ def channel(name, channel_options = {})
150
+ channels.get(name, channel_options)
151
+ end
152
+
153
+ # Retrieves the time from the Ably service as milliseconds since the Unix epoch. Clients that do not have access
154
+ # to a sufficiently well maintained time source and wish to issue Ably {Ably::Models::TokenRequests} with
155
+ # a more accurate timestamp should use the queryTime property instead of this method.
156
+ #
157
+ # @spec RTC6a
158
+ #
159
+ # @yield [Time] The time as milliseconds since the Unix epoch.
160
+ # @return [Ably::Util::SafeDeferrable]
161
+ #
162
+ def time(&success_callback)
163
+ async_wrap(success_callback) do
164
+ rest_client.time
165
+ end
166
+ end
167
+
168
+ # Queries the REST /stats API and retrieves your application's usage statistics.
169
+ # Returns a {Ably::Util::SafeDeferrable} object, containing an array of {Ably::Models::Stats} objects. See the Stats docs.
170
+ #
171
+ # @spec RTC5
172
+ #
173
+ # @param (see Ably::Rest::Client#stats)
174
+ # @option options (see Ably::Rest::Client#stats)
175
+ #
176
+ # @yield [Ably::Models::PaginatedResult<Ably::Models::Stats>] A {Ably::Util::SafeDeferrable} object containing an array of {Ably::Models::Stats} objects.
177
+ #
178
+ # @return [Ably::Util::SafeDeferrable]
179
+ #
180
+ def stats(options = {}, &success_callback)
181
+ async_wrap(success_callback) do
182
+ rest_client.stats(options)
183
+ end
184
+ end
185
+
186
+ # Calls {Connection#close} and causes the connection to close, entering the closing state.
187
+ # Once closed, the library will not attempt to re-establish the connection without an explicit call to {Connection#connect}.
188
+ # @spec RTN12
189
+ # (see Ably::Realtime::Connection#close)
190
+ def close(&block)
191
+ connection.close(&block)
192
+ end
193
+
194
+ # Calls {Ably::Realtime::Connection#connect} and causes the connection to open, entering the connecting
195
+ # state. Explicitly calling connect() is unnecessary unless the autoConnect property is disabled.
196
+ # @spec RTN11
197
+ # (see Ably::Realtime::Connection#connect)
198
+ def connect(&block)
199
+ connection.connect(&block)
200
+ end
201
+
202
+ # A {Ably::Realtime::Push} object.
203
+ # @return [Ably::Realtime::Push]
204
+ def push
205
+ @push ||= Push.new(self)
206
+ end
207
+
208
+ # Makes a REST request to a provided path. This is provided as a convenience for developers who wish to use REST
209
+ # API functionality that is either not documented or is not yet included in the public API, without having to
210
+ # directly handle features such as authentication, paging, fallback hosts, MsgPack and JSON support.
211
+ #
212
+ # @spec RTC9
213
+ #
214
+ # (see {Ably::Rest::Client#request})
215
+ # @yield [Ably::Models::HttpPaginatedResponse<>] An Array of Stats
216
+ #
217
+ # @return [Ably::Util::SafeDeferrable] An {Ably::Util::SafeDeferrable} response object returned by the HTTP request, containing an empty or JSON-encodable object.
218
+ def request(method, path, params = {}, body = nil, headers = {}, &callback)
219
+ async_wrap(callback) do
220
+ rest_client.request(method, path, params, body, headers, async_blocking_operations: true)
221
+ end
222
+ end
223
+
224
+ # Publish one or more messages to the specified channel.
225
+ #
226
+ # This method allows messages to be efficiently published to Ably without instancing a {Ably::Realtime::Channel} object.
227
+ # If you want to publish a high rate of messages to Ably without instancing channels or using the REST API, then this method
228
+ # is recommended. However, channel options such as encryption are not supported with this method. If you need to specify channel options
229
+ # we recommend you use the {Ably::Realtime::Channel} +publish+ method without attaching to each channel, unless you also want to subscribe
230
+ # to published messages on that channel.
231
+ #
232
+ # Note: This feature is still in beta. As such, we cannot guarantee the API will not change in future.
233
+ #
234
+ # @param channel [String] The channel name you want to publish the message(s) to
235
+ # @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
236
+ # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
237
+ # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string
238
+ #
239
+ # @yield [Ably::Models::Message,Array<Ably::Models::Message>] On success, will call the block with the {Ably::Models::Message} if a single message is published, or an Array of {Ably::Models::Message} when multiple messages are published
240
+ # @return [Ably::Util::SafeDeferrable] Deferrable that supports both success (callback) and failure (errback) callbacks
241
+ #
242
+ # @example
243
+ # # Publish a single message
244
+ # client.publish 'activityChannel', click', { x: 1, y: 2 }
245
+ #
246
+ # # Publish an array of message Hashes
247
+ # messages = [
248
+ # { name: 'click', { x: 1, y: 2 } },
249
+ # { name: 'click', { x: 2, y: 3 } }
250
+ # ]
251
+ # client.publish 'activityChannel', messages
252
+ #
253
+ # # Publish an array of Ably::Models::Message objects
254
+ # messages = [
255
+ # Ably::Models::Message(name: 'click', { x: 1, y: 2 })
256
+ # Ably::Models::Message(name: 'click', { x: 2, y: 3 })
257
+ # ]
258
+ # client.publish 'activityChannel', messages
259
+ #
260
+ # client.publish('activityChannel', 'click', 'body') do |message|
261
+ # puts "#{message.name} event received with #{message.data}"
262
+ # end
263
+ #
264
+ # client.publish('activityChannel', 'click', 'body').errback do |error, message|
265
+ # puts "#{message.name} was not received, error #{error.message}"
266
+ # end
267
+ #
268
+ def publish(channel_name, name, data = nil, attributes = {}, &success_block)
269
+ if !connection.can_publish_messages?
270
+ error = Ably::Exceptions::MessageQueueingDisabled.new("Message cannot be published. Client is not allowed to queue messages when connection is in state #{connection.state}")
271
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
272
+ end
273
+
274
+ messages = if name.kind_of?(Enumerable)
275
+ name
276
+ else
277
+ name = ensure_utf_8(:name, name, allow_nil: true)
278
+ ensure_supported_payload data
279
+ [{ name: name, data: data }.merge(attributes)]
280
+ end
281
+
282
+ if messages.length > Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE
283
+ error = Ably::Exceptions::InvalidRequest.new("It is not possible to publish more than #{Realtime::Connection::MAX_PROTOCOL_MESSAGE_BATCH_SIZE} messages with a single publish request.")
284
+ return Ably::Util::SafeDeferrable.new_and_fail_immediately(logger, error)
285
+ end
286
+
287
+ enqueue_messages_on_connection(self, messages, channel_name).tap do |deferrable|
288
+ deferrable.callback(&success_block) if block_given?
289
+ end
290
+ end
291
+
292
+ # @!attribute [r] endpoint
293
+ # @return [URI::Generic] Default Ably Realtime endpoint used for all requests
294
+ def endpoint
295
+ endpoint_for_host(custom_realtime_host || [environment, DOMAIN].compact.join('-'))
296
+ end
297
+
298
+ # (see Ably::Rest::Client#register_encoder)
299
+ def register_encoder(encoder)
300
+ rest_client.register_encoder encoder
301
+ end
302
+
303
+ # (see Ably::Rest::Client#fallback_hosts)
304
+ def fallback_hosts
305
+ rest_client.fallback_hosts
306
+ end
307
+
308
+ # (see Ably::Rest::Client#logger)
309
+ def logger
310
+ @logger ||= Ably::Logger.new(self, log_level, rest_client.logger.custom_logger)
311
+ end
312
+
313
+ # Disable connection recovery, typically used after a connection has been recovered
314
+ # @return [void]
315
+ # @api private
316
+ def disable_automatic_connection_recovery
317
+ @recover = nil
318
+ end
319
+
320
+ # @!attribute [r] fallback_endpoint
321
+ # @return [URI::Generic] Fallback endpoint used to connect to the realtime Ably service. Note, after each connection attempt, a new random {Ably::FALLBACK_HOSTS fallback host} or provided fallback hosts are used
322
+ # @api private
323
+ def fallback_endpoint
324
+ unless defined?(@fallback_endpoints) && @fallback_endpoints
325
+ @fallback_endpoints = fallback_hosts.shuffle.map { |fallback_host| endpoint_for_host(fallback_host) }
326
+ @fallback_endpoints << endpoint # Try the original host last if all fallbacks have been used
327
+ end
328
+
329
+ fallback_endpoint_index = connection.manager.retry_count_for_state(:disconnected) + connection.manager.retry_count_for_state(:suspended) - 1
330
+
331
+ @fallback_endpoints[fallback_endpoint_index % @fallback_endpoints.count]
332
+ end
333
+
334
+ # Retrieves a {Ably::Models::LocalDevice} object that represents the current state of the device as a target for push notifications.
335
+ # @spec RSH8
336
+ # @return [Ably::Models::LocalDevice] A {Ably::Models::LocalDevice} object.
337
+ #
338
+ # @note This is unsupported in the Ruby library
339
+ def device
340
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. The local device object is not unavailable'
341
+ end
342
+
343
+ private
344
+ def endpoint_for_host(host)
345
+ port = if use_tls?
346
+ custom_tls_port
347
+ else
348
+ custom_port
349
+ end
350
+
351
+ raise ArgumentError, "Custom port must be an Integer or nil" if port && !port.kind_of?(Integer)
352
+
353
+ options = {
354
+ scheme: use_tls? ? 'wss' : 'ws',
355
+ host: host
356
+ }
357
+ options.merge!(port: port) if port
358
+
359
+ URI::Generic.build(options)
360
+ end
361
+ end
362
+ end
363
+ end