Skip to content

Commit 1596829

Browse files
authored
Merge pull request #423 from ably/feature/integration-protocol-2
[ECO-4058] Feature/Integration protocol 2
2 parents cb684b4 + 3e1882e commit 1596829

37 files changed

+1095
-1082
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
_[Ably](https://ably.com) is the platform that powers synchronized digital experiences in realtime. Whether attending an event in a virtual venue, receiving realtime financial information, or monitoring live car performance data – consumers simply expect realtime digital experiences as standard. Ably provides a suite of APIs to build, extend, and deliver powerful digital experiences in realtime for more than 250 million devices across 80 countries each month. Organizations like Bloomberg, HubSpot, Verizon, and Hopin depend on Ably’s platform to offload the growing complexity of business-critical realtime data synchronization at global scale. For more information, see the [Ably documentation](https://ably.com/documentation)._
99

10-
This is a Ruby client library for Ably. The library currently targets the [Ably 1.2 client library specification](https://ably.com/documentation/client-lib-development-guide/features/). You can see the complete list of features this client library supports in [our client library SDKs feature support matrix](https://ably.com/download/sdk-feature-support-matrix).
10+
This is a Ruby client library for Ably. The library currently targets the [Ably 2.0.0 client library specification](https://ably.com/documentation/client-lib-development-guide/features/). You can see the complete list of features this client library supports in [our client library SDKs feature support matrix](https://ably.com/download/sdk-feature-support-matrix).
1111

1212
## Supported platforms
1313

lib/ably/auth.rb

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -414,13 +414,20 @@ def auth_header
414414
#
415415
# @return [Hash] headers
416416
def extra_auth_headers
417-
if client_id && using_basic_auth?
418-
{ 'X-Ably-ClientId' => Base64.urlsafe_encode64(client_id) }
417+
if client_id_for_request
418+
{ 'X-Ably-ClientId' => Base64.urlsafe_encode64(client_id_for_request) }
419419
else
420420
{}
421421
end
422422
end
423423

424+
# ClientId that needs to be included with every rest/realtime request
425+
# spec - RSA7e
426+
# @return string
427+
def client_id_for_request
428+
options[:client_id] if options[:client_id] && using_basic_auth?
429+
end
430+
424431
# Auth params used in URI endpoint for Realtime connections
425432
# Will reauthorize implicitly if required and capable
426433
#
@@ -482,15 +489,16 @@ def can_assume_client_id?(assumed_client_id)
482489
#
483490
# @api private
484491
def configure_client_id(new_client_id)
485-
# If new client ID from Ably is a wildcard, but preconfigured clientId is set, then keep the existing clientId
486-
if has_client_id? && new_client_id == '*'
487-
@client_id_validated = true
488-
return
489-
end
490-
491-
# If client_id is defined and not a wildcard, prevent it changing, this is not supported
492-
if client_id && client_id != '*' && new_client_id != client_id
493-
raise Ably::Exceptions::IncompatibleClientId.new("Client ID is immutable once configured for a client. Client ID cannot be changed to '#{new_client_id}'")
492+
if has_client_id?
493+
# If new client ID from Ably is a wildcard, but preconfigured clientId is set, then keep the existing clientId
494+
if new_client_id == "*"
495+
@client_id_validated = true
496+
return
497+
end
498+
# If client_id is defined and not a wildcard, prevent it changing, this is not supported
499+
if new_client_id != client_id
500+
raise Ably::Exceptions::IncompatibleClientId.new("Client ID is immutable once configured for a client. Client ID cannot be changed to '#{new_client_id}'")
501+
end
494502
end
495503
@client_id_validated = true
496504
@client_id = new_client_id

lib/ably/models/protocol_message.rb

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ module Ably::Models
2020
# @return [String] Contains a serial number for a message on the current channel
2121
# @!attribute [r] connection_id
2222
# @return [String] Contains a string private connection key used to recover this connection
23-
# @!attribute [r] connection_serial
24-
# @return [Bignum] Contains a serial number for a message sent from the server to the client
2523
# @!attribute [r] message_serial
2624
# @return [Bignum] Contains a serial number for a message sent from the client to the server
2725
# @!attribute [r] timestamp
@@ -129,12 +127,6 @@ def message_serial
129127
raise TypeError, "msg_serial '#{attributes[:msg_serial]}' is invalid, a positive Integer is expected for a ProtocolMessage"
130128
end
131129

132-
def connection_serial
133-
Integer(attributes[:connection_serial])
134-
rescue TypeError
135-
raise TypeError, "connection_serial '#{attributes[:connection_serial]}' is invalid, a positive Integer is expected for a ProtocolMessage"
136-
end
137-
138130
def count
139131
[1, attributes[:count].to_i].max
140132
end
@@ -146,26 +138,12 @@ def has_message_serial?
146138
false
147139
end
148140

149-
# @api private
150-
def has_connection_serial?
151-
connection_serial && true
141+
def has_channel_serial?
142+
channel_serial && true
152143
rescue TypeError
153144
false
154145
end
155146

156-
def serial
157-
if has_connection_serial?
158-
connection_serial
159-
else
160-
message_serial
161-
end
162-
end
163-
164-
# @api private
165-
def has_serial?
166-
has_connection_serial? || has_message_serial?
167-
end
168-
169147
def messages
170148
@messages ||=
171149
Array(attributes[:messages]).map do |message|
@@ -271,7 +249,7 @@ def attributes
271249
# Return a JSON ready object from the underlying #attributes using Ably naming conventions for keys
272250
def as_json(*args)
273251
raise TypeError, ':action is missing, cannot generate a valid Hash for ProtocolMessage' unless action
274-
raise TypeError, ':msg_serial or :connection_serial is missing, cannot generate a valid Hash for ProtocolMessage' if ack_required? && !has_serial?
252+
raise TypeError, ':msg_serial is missing, cannot generate a valid Hash for ProtocolMessage' if ack_required? && !has_message_serial?
275253

276254
attributes.dup.tap do |hash_object|
277255
hash_object['action'] = action.to_i
@@ -296,11 +274,12 @@ def to_s
296274
end
297275

298276
# True if the ProtocolMessage appears to be invalid, however this is not a guarantee
277+
# Used for validating incoming protocol messages, so no need to add unnecessary checks
299278
# @return [Boolean]
300279
# @api private
301280
def invalid?
302281
action_enum = action rescue nil
303-
!action_enum || (ack_required? && !has_serial?)
282+
!action_enum
304283
end
305284

306285
# @!attribute [r] logger

lib/ably/modules/safe_deferrable.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def errback(&block)
3939
end
4040
end
4141

42-
# Mark the Deferrable as succeeded and trigger all callbacks.
42+
# Mark the Deferrable as succeeded and trigger all success callbacks.
4343
# See http://www.rubydoc.info/gems/eventmachine/1.0.7/EventMachine/Deferrable#succeed-instance_method
4444
#
4545
# @return [void]
@@ -48,7 +48,7 @@ def succeed(*args)
4848
super(*args)
4949
end
5050

51-
# Mark the Deferrable as failed and trigger all callbacks.
51+
# Mark the Deferrable as failed and trigger all error callbacks.
5252
# See http://www.rubydoc.info/gems/eventmachine/1.0.7/EventMachine/Deferrable#fail-instance_method
5353
#
5454
# @return [void]

lib/ably/modules/state_emitter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Ably::Modules
33
# the instance variable @state is used exclusively, the {Enum} STATE is defined prior to inclusion of this
44
# module, and the class is an {EventEmitter}. It then emits state changes.
55
#
6-
# It also ensures the EventEmitter is configured to retrict permitted events to the
6+
# It also ensures the EventEmitter is configured to restrict permitted events to the
77
# the available STATEs or EVENTs if defined i.e. if EVENTs includes an additional type such as
88
# :update, then it will support all EVENTs being emitted. EVENTs must be a superset of STATEs
99
#

lib/ably/realtime/auth.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ def auth_header_sync
226226
auth_sync.auth_header
227227
end
228228

229+
def client_id_for_request_sync
230+
auth_sync.client_id_for_request
231+
end
232+
229233
# Auth params used in URI endpoint for Realtime connections
230234
# Will reauthorize implicitly if required and capable
231235
#

lib/ably/realtime/channel.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class Channel
4242
#
4343
# @spec RTL2b
4444
#
45-
# The permited states for this channel
45+
# The permitted states for this channel
4646
STATE = ruby_enum('STATE',
4747
:initialized,
4848
:attaching,
@@ -364,8 +364,8 @@ def __incoming_msgbus__
364364
# @return [Ably::Models::ChannelOptions]
365365
def set_options(channel_options)
366366
@options = Ably::Models::ChannelOptions(channel_options)
367-
368-
manager.request_reattach if need_reattach?
367+
# RTL4i
368+
manager.request_reattach if (need_reattach? and connection.state?(:connected))
369369
end
370370
alias options= set_options
371371

lib/ably/realtime/channel/channel_manager.rb

Lines changed: 51 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def initialize(channel, connection)
1818
def attach
1919
if can_transition_to?(:attached)
2020
connect_if_connection_initialized
21-
send_attach_protocol_message
21+
send_attach_protocol_message if connection.state?(:connected) # RTL4i
2222
end
2323
end
2424

@@ -34,9 +34,9 @@ def detach(error, previous_state)
3434
# Channel is attached, notify presence if sync is expected
3535
def attached(attached_protocol_message)
3636
# If no attached ProtocolMessage then this attached request was triggered by the client
37-
# library, such as returning to attached whne detach has failed
37+
# library, such as returning to attached when detach has failed
3838
if attached_protocol_message
39-
update_presence_sync_state_following_attached attached_protocol_message
39+
channel.presence.manager.on_attach attached_protocol_message.has_presence_flag?
4040
channel.properties.set_attach_serial(attached_protocol_message.channel_serial)
4141
channel.options.set_modes_from_flags(attached_protocol_message.flags)
4242
channel.options.set_params(attached_protocol_message.params)
@@ -49,17 +49,16 @@ def log_channel_error(error)
4949
end
5050

5151
# Request channel to be reattached by sending an attach protocol message
52-
# @param [Hash] options
53-
# @option options [Ably::Models::ErrorInfo] :reason
54-
def request_reattach(options = {})
55-
reason = options[:reason]
56-
send_attach_protocol_message
57-
logger.debug { "Explicit channel reattach request sent to Ably due to #{reason}" }
52+
# @param [Ably::Models::ErrorInfo] reason
53+
def request_reattach(reason = nil)
5854
channel.set_channel_error_reason(reason) if reason
5955
channel.transition_state_machine! :attaching, reason: reason unless channel.attaching?
56+
send_attach_protocol_message
57+
logger.debug { "Explicit channel reattach request sent to Ably due to #{reason}" }
6058
end
6159

6260
def duplicate_attached_received(protocol_message)
61+
logger.debug { "Server initiated ATTACHED message received for channel '#{channel.name}' with state #{channel.state}" }
6362
if protocol_message.error
6463
channel.set_channel_error_reason protocol_message.error
6564
log_channel_error protocol_message.error
@@ -68,17 +67,15 @@ def duplicate_attached_received(protocol_message)
6867
channel.properties.set_attach_serial(protocol_message.channel_serial)
6968
channel.options.set_modes_from_flags(protocol_message.flags)
7069

71-
if protocol_message.has_channel_resumed_flag?
72-
logger.debug { "ChannelManager: Additional resumed ATTACHED message received for #{channel.state} channel '#{channel.name}'" }
73-
else
70+
unless protocol_message.has_channel_resumed_flag?
7471
channel.emit :update, Ably::Models::ChannelStateChange.new(
7572
current: channel.state,
7673
previous: channel.state,
7774
event: Ably::Realtime::Channel::EVENT(:update),
7875
reason: protocol_message.error,
7976
resumed: false,
8077
)
81-
update_presence_sync_state_following_attached protocol_message
78+
channel.presence.manager.on_attach protocol_message.has_presence_flag?
8279
end
8380
end
8481

@@ -170,6 +167,12 @@ def start_attach_from_suspended_timer
170167
end
171168
end
172169

170+
# RTL13c
171+
def notify_state_change
172+
@pending_state_change_timer.cancel if @pending_state_change_timer
173+
@pending_state_change_timer = nil
174+
end
175+
173176
private
174177
attr_reader :pending_state_change_timer
175178

@@ -209,56 +212,56 @@ def send_attach_protocol_message
209212
message_options[:flags] = message_options[:flags].to_i | Ably::Models::ProtocolMessage::ATTACH_FLAGS_MAPPING[:resume]
210213
end
211214

212-
send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Attach, :suspended, message_options
213-
end
214-
215-
def send_detach_protocol_message(previous_state)
216-
send_state_change_protocol_message Ably::Models::ProtocolMessage::ACTION.Detach, previous_state # return to previous state if failed
217-
end
215+
message_options[:channelSerial] = channel.properties.channel_serial # RTL4c1
218216

219-
def send_state_change_protocol_message(new_state, state_if_failed, message_options = {})
220217
state_at_time_of_request = channel.state
218+
attach_action = Ably::Models::ProtocolMessage::ACTION.Attach
219+
# RTL4f
221220
@pending_state_change_timer = EventMachine::Timer.new(realtime_request_timeout) do
222221
if channel.state == state_at_time_of_request
223-
error = Ably::Models::ErrorInfo.new(code: Ably::Exceptions::Codes::CHANNEL_OPERATION_FAILED_NO_RESPONSE_FROM_SERVER, message: "Channel #{new_state} operation failed (timed out)")
224-
channel.transition_state_machine state_if_failed, reason: error
222+
error = Ably::Models::ErrorInfo.new(code: Ably::Exceptions::Codes::CHANNEL_OPERATION_FAILED_NO_RESPONSE_FROM_SERVER, message: "Channel #{attach_action} operation failed (timed out)")
223+
channel.transition_state_machine :suspended, reason: error # return to suspended state if failed
225224
end
226225
end
226+
# Shouldn't queue attach message as per RTL4i, so message is added top of the queue
227+
# to be sent immediately while processing next message
228+
connection.send_protocol_message_immediately(
229+
action: attach_action.to_i,
230+
channel: channel.name,
231+
**message_options.to_h
232+
)
233+
end
227234

228-
channel.once_state_changed do
229-
@pending_state_change_timer.cancel if @pending_state_change_timer
230-
@pending_state_change_timer = nil
235+
def send_detach_protocol_message(previous_state)
236+
state_at_time_of_request = channel.state
237+
detach_action = Ably::Models::ProtocolMessage::ACTION.Detach
238+
239+
@pending_state_change_timer = EventMachine::Timer.new(realtime_request_timeout) do
240+
if channel.state == state_at_time_of_request
241+
error = Ably::Models::ErrorInfo.new(code: Ably::Exceptions::Codes::CHANNEL_OPERATION_FAILED_NO_RESPONSE_FROM_SERVER, message: "Channel #{detach_action} operation failed (timed out)")
242+
channel.transition_state_machine previous_state, reason: error # return to previous state if failed
243+
end
231244
end
232245

233-
resend_if_disconnected_and_connected = lambda do
246+
on_disconnected_and_connected = lambda do
234247
connection.unsafe_once(:disconnected) do
235-
next unless pending_state_change_timer
236248
connection.unsafe_once(:connected) do
237-
next unless pending_state_change_timer
238-
connection.send_protocol_message(
239-
action: new_state.to_i,
240-
channel: channel.name,
241-
**message_options.to_h
242-
)
243-
resend_if_disconnected_and_connected.call
244-
end
249+
yield if pending_state_change_timer
250+
end if pending_state_change_timer
245251
end
246252
end
247-
resend_if_disconnected_and_connected.call
248-
249-
connection.send_protocol_message(
250-
action: new_state.to_i,
251-
channel: channel.name,
252-
**message_options.to_h
253-
)
254-
end
255253

256-
def update_presence_sync_state_following_attached(attached_protocol_message)
257-
if attached_protocol_message.has_presence_flag?
258-
channel.presence.manager.sync_expected
259-
else
260-
channel.presence.manager.sync_not_expected
254+
send_detach_message = lambda do
255+
on_disconnected_and_connected.call do
256+
send_detach_message.call
257+
end
258+
connection.send_protocol_message(
259+
action: detach_action.to_i,
260+
channel: channel.name
261+
)
261262
end
263+
264+
send_detach_message.call
262265
end
263266

264267
def logger

lib/ably/realtime/channel/channel_properties.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ class ChannelProperties
1818
#
1919
attr_reader :attach_serial
2020

21+
# ChannelSerial contains the channelSerial from latest ProtocolMessage of action type
22+
# Message/PresenceMessage received on the channel.
23+
#
24+
# @spec CP2b, RTL15b
25+
#
26+
# @return [String]
27+
#
28+
attr_accessor :channel_serial
29+
2130
def initialize(channel)
2231
@channel = channel
2332
end

lib/ably/realtime/channel/channel_state_machine.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class ChannelStateMachine
2929
transition :from => :failed, :to => [:attaching, :initialized]
3030

3131
after_transition do |channel, transition|
32+
channel.manager.notify_state_change # RTL13c
3233
channel.synchronize_state_with_statemachine
3334
end
3435

@@ -55,6 +56,7 @@ class ChannelStateMachine
5556
end
5657

5758
after_transition(to: [:detached, :failed, :suspended]) do |channel, current_transition|
59+
channel.properties.channel_serial = nil # RTP5a1
5860
err = error_from_state_change(current_transition)
5961
channel.manager.fail_queued_messages(err) if channel.failed? or channel.suspended? #RTL11
6062
channel.manager.log_channel_error err if err

0 commit comments

Comments
 (0)