ably 0.6.2 → 0.7.0

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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version.old +1 -0
  4. data/.travis.yml +0 -2
  5. data/Rakefile +22 -4
  6. data/SPEC.md +1676 -0
  7. data/ably.gemspec +1 -1
  8. data/lib/ably.rb +0 -8
  9. data/lib/ably/auth.rb +54 -46
  10. data/lib/ably/exceptions.rb +19 -5
  11. data/lib/ably/logger.rb +1 -1
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/idiomatic_ruby_wrapper.rb +11 -9
  14. data/lib/ably/models/message.rb +15 -12
  15. data/lib/ably/models/message_encoders/base.rb +6 -5
  16. data/lib/ably/models/message_encoders/base64.rb +1 -0
  17. data/lib/ably/models/message_encoders/cipher.rb +6 -3
  18. data/lib/ably/models/message_encoders/json.rb +1 -0
  19. data/lib/ably/models/message_encoders/utf8.rb +2 -9
  20. data/lib/ably/models/nil_logger.rb +20 -0
  21. data/lib/ably/models/paginated_resource.rb +5 -2
  22. data/lib/ably/models/presence_message.rb +21 -12
  23. data/lib/ably/models/protocol_message.rb +22 -6
  24. data/lib/ably/modules/ably.rb +11 -0
  25. data/lib/ably/modules/async_wrapper.rb +2 -0
  26. data/lib/ably/modules/conversions.rb +23 -3
  27. data/lib/ably/modules/encodeable.rb +2 -1
  28. data/lib/ably/modules/enum.rb +2 -0
  29. data/lib/ably/modules/event_emitter.rb +7 -1
  30. data/lib/ably/modules/event_machine_helpers.rb +2 -0
  31. data/lib/ably/modules/http_helpers.rb +2 -0
  32. data/lib/ably/modules/model_common.rb +12 -2
  33. data/lib/ably/modules/state_emitter.rb +76 -0
  34. data/lib/ably/modules/state_machine.rb +53 -0
  35. data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  36. data/lib/ably/modules/uses_state_machine.rb +74 -0
  37. data/lib/ably/realtime.rb +4 -2
  38. data/lib/ably/realtime/channel.rb +51 -58
  39. data/lib/ably/realtime/channel/channel_manager.rb +91 -0
  40. data/lib/ably/realtime/channel/channel_state_machine.rb +68 -0
  41. data/lib/ably/realtime/client.rb +70 -26
  42. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +31 -13
  43. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  44. data/lib/ably/realtime/connection.rb +135 -92
  45. data/lib/ably/realtime/connection/connection_manager.rb +216 -33
  46. data/lib/ably/realtime/connection/connection_state_machine.rb +30 -73
  47. data/lib/ably/realtime/models/nil_channel.rb +10 -1
  48. data/lib/ably/realtime/presence.rb +336 -92
  49. data/lib/ably/rest.rb +2 -2
  50. data/lib/ably/rest/channel.rb +13 -4
  51. data/lib/ably/rest/client.rb +138 -38
  52. data/lib/ably/rest/middleware/logger.rb +24 -3
  53. data/lib/ably/rest/presence.rb +12 -7
  54. data/lib/ably/version.rb +1 -1
  55. data/spec/acceptance/realtime/channel_history_spec.rb +101 -85
  56. data/spec/acceptance/realtime/channel_spec.rb +461 -120
  57. data/spec/acceptance/realtime/client_spec.rb +119 -0
  58. data/spec/acceptance/realtime/connection_failures_spec.rb +499 -0
  59. data/spec/acceptance/realtime/connection_spec.rb +571 -97
  60. data/spec/acceptance/realtime/message_spec.rb +347 -333
  61. data/spec/acceptance/realtime/presence_history_spec.rb +35 -40
  62. data/spec/acceptance/realtime/presence_spec.rb +769 -239
  63. data/spec/acceptance/realtime/stats_spec.rb +14 -22
  64. data/spec/acceptance/realtime/time_spec.rb +16 -20
  65. data/spec/acceptance/rest/auth_spec.rb +425 -364
  66. data/spec/acceptance/rest/base_spec.rb +108 -176
  67. data/spec/acceptance/rest/channel_spec.rb +89 -89
  68. data/spec/acceptance/rest/channels_spec.rb +30 -32
  69. data/spec/acceptance/rest/client_spec.rb +273 -0
  70. data/spec/acceptance/rest/encoders_spec.rb +185 -0
  71. data/spec/acceptance/rest/message_spec.rb +186 -163
  72. data/spec/acceptance/rest/presence_spec.rb +150 -111
  73. data/spec/acceptance/rest/stats_spec.rb +45 -40
  74. data/spec/acceptance/rest/time_spec.rb +8 -10
  75. data/spec/rspec_config.rb +10 -1
  76. data/spec/shared/client_initializer_behaviour.rb +212 -0
  77. data/spec/{support/model_helper.rb → shared/model_behaviour.rb} +6 -6
  78. data/spec/{support/protocol_msgbus_helper.rb → shared/protocol_msgbus_behaviour.rb} +1 -1
  79. data/spec/spec_helper.rb +9 -0
  80. data/spec/support/api_helper.rb +11 -0
  81. data/spec/support/event_machine_helper.rb +101 -3
  82. data/spec/support/markdown_spec_formatter.rb +90 -0
  83. data/spec/support/private_api_formatter.rb +36 -0
  84. data/spec/support/protocol_helper.rb +32 -0
  85. data/spec/support/random_helper.rb +15 -0
  86. data/spec/support/test_app.rb +4 -0
  87. data/spec/unit/auth_spec.rb +68 -0
  88. data/spec/unit/logger_spec.rb +77 -66
  89. data/spec/unit/models/error_info_spec.rb +1 -1
  90. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +2 -3
  91. data/spec/unit/models/message_encoders/base64_spec.rb +2 -2
  92. data/spec/unit/models/message_encoders/cipher_spec.rb +2 -2
  93. data/spec/unit/models/message_encoders/utf8_spec.rb +2 -46
  94. data/spec/unit/models/message_spec.rb +160 -15
  95. data/spec/unit/models/paginated_resource_spec.rb +29 -27
  96. data/spec/unit/models/presence_message_spec.rb +163 -20
  97. data/spec/unit/models/protocol_message_spec.rb +43 -8
  98. data/spec/unit/modules/async_wrapper_spec.rb +2 -3
  99. data/spec/unit/modules/conversions_spec.rb +1 -1
  100. data/spec/unit/modules/enum_spec.rb +2 -3
  101. data/spec/unit/modules/event_emitter_spec.rb +62 -5
  102. data/spec/unit/modules/state_emitter_spec.rb +283 -0
  103. data/spec/unit/realtime/channel_spec.rb +107 -2
  104. data/spec/unit/realtime/channels_spec.rb +1 -0
  105. data/spec/unit/realtime/client_spec.rb +8 -48
  106. data/spec/unit/realtime/connection_spec.rb +3 -3
  107. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +2 -2
  108. data/spec/unit/realtime/presence_spec.rb +13 -4
  109. data/spec/unit/realtime/realtime_spec.rb +0 -11
  110. data/spec/unit/realtime/websocket_transport_spec.rb +2 -2
  111. data/spec/unit/rest/channel_spec.rb +109 -0
  112. data/spec/unit/rest/channels_spec.rb +4 -3
  113. data/spec/unit/rest/client_spec.rb +30 -125
  114. data/spec/unit/rest/rest_spec.rb +10 -0
  115. data/spec/unit/util/crypto_spec.rb +10 -5
  116. data/spec/unit/util/pub_sub_spec.rb +5 -5
  117. metadata +44 -12
  118. data/spec/integration/modules/state_emitter_spec.rb +0 -80
  119. data/spec/integration/rest/auth.rb +0 -9
@@ -1,4 +1,5 @@
1
1
  require 'json'
2
+ require 'ably/models/message_encoders/base'
2
3
 
3
4
  module Ably::Models::MessageEncoders
4
5
  # JSON Encoder and Decoder
@@ -1,4 +1,4 @@
1
- require 'json'
1
+ require 'ably/models/message_encoders/base'
2
2
 
3
3
  module Ably::Models::MessageEncoders
4
4
  # Utf8 Encoder and Decoder
@@ -8,10 +8,7 @@ module Ably::Models::MessageEncoders
8
8
  ENCODING_ID = 'utf-8'
9
9
 
10
10
  def encode(message, channel_options)
11
- if is_json_encoded?(message)
12
- message[:data] = message[:data].force_encoding(Encoding::UTF_8)
13
- add_encoding_to_message ENCODING_ID, message
14
- end
11
+ # no encoding of UTF-8 required
15
12
  end
16
13
 
17
14
  def decode(message, channel_options)
@@ -25,9 +22,5 @@ module Ably::Models::MessageEncoders
25
22
  def is_utf8_encoded?(message)
26
23
  current_encoding_part(message).to_s.match(/^#{ENCODING_ID}$/i)
27
24
  end
28
-
29
- def is_json_encoded?(message)
30
- current_encoding_part(message).to_s.match(/^json$/i)
31
- end
32
25
  end
33
26
  end
@@ -0,0 +1,20 @@
1
+ module Ably::Models
2
+ # When Log Level set to none, this NilLogger is used to silence all logging
3
+ # NilLogger provides a Ruby Logger compatible interface
4
+ class NilLogger
5
+ def null_method(*args)
6
+ end
7
+
8
+ def level
9
+ :none
10
+ end
11
+
12
+ def level=(value)
13
+ level
14
+ end
15
+
16
+ [:fatal, :error, :warn, :info, :debug].each do |method|
17
+ alias_method method, :null_method
18
+ end
19
+ end
20
+ end
@@ -5,7 +5,7 @@ module Ably::Models
5
5
  # Paging information is provided by Ably in the LINK HTTP headers
6
6
  class PaginatedResource
7
7
  include Enumerable
8
- include Ably::Modules::AsyncWrapper
8
+ include Ably::Modules::AsyncWrapper if defined?(EventMachine)
9
9
 
10
10
  # @param [Faraday::Response] http_response Initial HTTP response from an Ably request to a paged resource
11
11
  # @param [String] base_url Base URL for request that generated the http_response so that subsequent paged requests can be made
@@ -27,7 +27,9 @@ module Ably::Models
27
27
 
28
28
  @body = if @coerce_into
29
29
  http_response.body.map do |item|
30
- Kernel.const_get(@coerce_into).new(item)
30
+ @coerce_into.split('::').inject(Kernel) do |base, klass_name|
31
+ base.public_send(:const_get, klass_name)
32
+ end.new(item)
31
33
  end
32
34
  else
33
35
  http_response.body
@@ -168,6 +170,7 @@ module Ably::Models
168
170
 
169
171
  def async_wrap_if(is_realtime, success_callback, &operation)
170
172
  if is_realtime
173
+ raise 'EventMachine is required for asynchronous operations' unless defined?(EventMachine)
171
174
  async_wrap success_callback, &operation
172
175
  else
173
176
  yield
@@ -23,8 +23,10 @@ module Ably::Models
23
23
  # @return [STATE] the state change event signified by a PresenceMessage
24
24
  # @!attribute [r] client_id
25
25
  # @return [String] The client_id associated with this presence state
26
- # @!attribute [r] member_id
26
+ # @!attribute [r] connection_id
27
27
  # @return [String] A unique member identifier, disambiguating situations where a given client_id is present on multiple connections simultaneously
28
+ # @!attribute [r] member_key
29
+ # @return [String] A unique connection and client_id identifier ensuring multiple connected clients with the same client_id are unique
28
30
  # @!attribute [r] data
29
31
  # @return [Object] Optional client-defined status or other event payload associated with this state
30
32
  # @!attribute [r] encoding
@@ -36,12 +38,15 @@ module Ably::Models
36
38
  # @return [Hash] Access the protocol message Hash object ruby'fied to use symbolized keys
37
39
  #
38
40
  class PresenceMessage
39
- include Ably::Modules::ModelCommon
41
+ include Ably::Modules::Conversions
40
42
  include Ably::Modules::Encodeable
43
+ include Ably::Modules::ModelCommon
41
44
  include EventMachine::Deferrable
42
45
  extend Ably::Modules::Enum
43
46
 
44
47
  ACTION = ruby_enum('ACTION',
48
+ :absent,
49
+ :present,
45
50
  :enter,
46
51
  :leave,
47
52
  :update
@@ -57,16 +62,28 @@ module Ably::Models
57
62
  @raw_hash_object = hash_object
58
63
 
59
64
  set_hash_object hash_object
65
+
66
+ ensure_utf_8 :client_id, client_id, allow_nil: true
67
+ ensure_utf_8 :connection_id, connection_id, allow_nil: true
68
+ ensure_utf_8 :encoding, encoding, allow_nil: true
60
69
  end
61
70
 
62
- %w( client_id member_id data encoding ).each do |attribute|
71
+ %w( client_id data encoding ).each do |attribute|
63
72
  define_method attribute do
64
73
  hash[attribute.to_sym]
65
74
  end
66
75
  end
67
76
 
68
77
  def id
69
- hash[:id] || "#{protocol_message.id!}:#{protocol_message_index}"
78
+ hash.fetch(:id) { "#{protocol_message.id!}:#{protocol_message_index}" }
79
+ end
80
+
81
+ def connection_id
82
+ hash.fetch(:connection_id) { protocol_message.connection_id if assigned_to_protocol_message? }
83
+ end
84
+
85
+ def member_key
86
+ "#{connection_id}:#{client_id}"
70
87
  end
71
88
 
72
89
  def timestamp
@@ -123,14 +140,6 @@ module Ably::Models
123
140
  protocol_message.presence.index(self)
124
141
  end
125
142
 
126
- def connection_id
127
- protocol_message.connection_id
128
- end
129
-
130
- def message_serial
131
- protocol_message.message_serial
132
- end
133
-
134
143
  def set_hash_object(hash)
135
144
  @hash_object = IdiomaticRubyWrapper(hash.clone.freeze, stop_at: [:data])
136
145
  end
@@ -7,7 +7,7 @@ module Ably::Models
7
7
  # for further details on the members of a ProtocolMessage
8
8
  #
9
9
  # @!attribute [r] action
10
- # @return [ACTION] Protocol Message action {Ably::Modules::Enum} from list of {ACTION}. Returns nil if action is unsupported by protocol.
10
+ # @return [ACTION] Protocol Message action {Ably::Modules::Enum} from list of {ACTION}. Returns nil if action is unsupported by protocol
11
11
  # @!attribute [r] count
12
12
  # @return [Integer] The count field is used for ACK and NACK actions. See {http://docs.ably.io/client-lib-development-guide/protocol/#message-acknowledgement message acknowledgement protocol}
13
13
  # @!attribute [r] error_info
@@ -17,7 +17,9 @@ module Ably::Models
17
17
  # @!attribute [r] channel_serial
18
18
  # @return [String] Contains a serial number for a message on the current channel
19
19
  # @!attribute [r] connection_id
20
- # @return [String] Contains a string connection ID
20
+ # @return [String] Contains a string public identifier for the connection
21
+ # @!attribute [r] connection_key
22
+ # @return [String] Contains a string private connection key used to recover this connection
21
23
  # @!attribute [r] connection_serial
22
24
  # @return [Bignum] Contains a serial number for a message sent from the server to the client
23
25
  # @!attribute [r] message_serial
@@ -25,9 +27,11 @@ module Ably::Models
25
27
  # @!attribute [r] timestamp
26
28
  # @return [Time] An optional timestamp, applied by the service in messages sent to the client, to indicate the system time at which the message was sent (milliseconds past epoch)
27
29
  # @!attribute [r] messages
28
- # @return [Message] A {ProtocolMessage} with a `:message` action contains one or more messages belonging to a channel.
30
+ # @return [Message] A {ProtocolMessage} with a `:message` action contains one or more messages belonging to a channel
29
31
  # @!attribute [r] presence
30
- # @return [PresenceMessage] A {ProtocolMessage} with a `:presence` action contains one or more presence updates belonging to a channel.
32
+ # @return [PresenceMessage] A {ProtocolMessage} with a `:presence` action contains one or more presence updates belonging to a channel
33
+ # @!attribute [r] flags
34
+ # @return [Integer] Flags inidicating special ProtocolMessage states
31
35
  # @!attribute [r] hash
32
36
  # @return [Hash] Access the protocol message Hash object ruby'fied to use symbolized keys
33
37
  #
@@ -56,7 +60,8 @@ module Ably::Models
56
60
  detach: 12,
57
61
  detached: 13,
58
62
  presence: 14,
59
- message: 15
63
+ message: 15,
64
+ sync: 16
60
65
  )
61
66
 
62
67
  # Indicates this protocol message action will generate an ACK response such as :message or :presence
@@ -74,7 +79,7 @@ module Ably::Models
74
79
  @hash_object.freeze
75
80
  end
76
81
 
77
- %w( id channel channel_serial connection_id ).each do |attribute|
82
+ %w(id channel channel_serial connection_id connection_key).each do |attribute|
78
83
  define_method attribute do
79
84
  hash[attribute.to_sym]
80
85
  end
@@ -157,6 +162,17 @@ module Ably::Models
157
162
  end
158
163
  end
159
164
 
165
+ # Flags as bits
166
+ def flags
167
+ Integer(hash[:flags])
168
+ rescue TypeError
169
+ 0
170
+ end
171
+
172
+ def has_presence_flag?
173
+ flags & 1 == 1
174
+ end
175
+
160
176
  # Indicates this protocol message will generate an ACK response when sent
161
177
  # Examples of protocol messages required ACK include :message and :presence
162
178
  def ack_required?
@@ -0,0 +1,11 @@
1
+ # Ably is the base namespace for the Ably {Ably::Realtime Realtime} & {Ably::Rest Rest} client libraries.
2
+ #
3
+ # Please refer to the {file:README.md Readme} on getting started.
4
+ #
5
+ # @see file:README.md README
6
+ module Ably
7
+ # Fallback hosts to use when a connection to rest/realtime.ably.io is not possible due to
8
+ # network failures either at the client, between the client and Ably, within an Ably data center, or at the IO domain registrar
9
+ #
10
+ FALLBACK_HOSTS = %w(A.ably-realtime.com B.ably-realtime.com C.ably-realtime.com D.ably-realtime.com E.ably-realtime.com)
11
+ end
@@ -1,3 +1,5 @@
1
+ require 'eventmachine'
2
+
1
3
  module Ably::Modules
2
4
  # Provides methods to convert synchronous operations into async operations through the use of
3
5
  # {EventMachine#defer http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine#defer-class_method}.
@@ -5,7 +5,9 @@ module Ably::Modules
5
5
  extend self
6
6
 
7
7
  private
8
- def as_since_epoch(time, granularity: :ms)
8
+ def as_since_epoch(time, options = {})
9
+ granularity = options.fetch(:granularity, :ms)
10
+
9
11
  case time
10
12
  when Time
11
13
  time.to_f * multiplier_from_granularity(granularity)
@@ -16,7 +18,9 @@ module Ably::Modules
16
18
  end.to_i
17
19
  end
18
20
 
19
- def as_time_from_epoch(time, granularity: :ms)
21
+ def as_time_from_epoch(time, options = {})
22
+ granularity = options.fetch(:granularity, :ms)
23
+
20
24
  case time
21
25
  when Numeric
22
26
  Time.at(time / multiplier_from_granularity(granularity))
@@ -39,7 +43,9 @@ module Ably::Modules
39
43
  end
40
44
 
41
45
  # Convert key to mixedCase from mixed_case
42
- def convert_to_mixed_case(key, force_camel: false)
46
+ def convert_to_mixed_case(key, options = {})
47
+ force_camel = options.fetch(:force_camel, false)
48
+
43
49
  key.to_s.
44
50
  split('_').
45
51
  each_with_index.map do |str, index|
@@ -66,5 +72,19 @@ module Ably::Modules
66
72
  def convert_to_lower_case(key)
67
73
  key.to_s.gsub('_', '')
68
74
  end
75
+
76
+ # Ensures that the string value is converted to UTF-8 encoding
77
+ # Unless option allow_nil: true, an {ArgumentError} is raised if the string_value is not a string
78
+ #
79
+ # @return <void>
80
+ #
81
+ def ensure_utf_8(field_name, string_value, options = {})
82
+ unless options[:allow_nil] && string_value.nil?
83
+ raise ArgumentError, "#{field_name} must be a String" unless string_value.kind_of?(String)
84
+ end
85
+ string_value.encode!(Encoding::UTF_8) if string_value
86
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
87
+ raise ArgumentError, "#{field_name} could not be converted to UTF-8: #{e.message}"
88
+ end
69
89
  end
70
90
  end
@@ -1,4 +1,5 @@
1
1
  require 'base64'
2
+ require 'ably/exceptions'
2
3
 
3
4
  module Ably::Modules
4
5
  # Provides methods to allow this model's `data` property to be encoded and decoded based on the `encoding` property.
@@ -57,8 +58,8 @@ module Ably::Modules
57
58
 
58
59
  set_hash_object message_hash
59
60
  rescue Ably::Exceptions::CipherError => cipher_error
60
- channel.client.logger.error "Encoder error #{cipher_error.code} trying to #{method} message: #{cipher_error.message}"
61
61
  if channel.respond_to?(:trigger)
62
+ channel.client.logger.error "Encoder error #{cipher_error.code} trying to #{method} message: #{cipher_error.message}"
62
63
  channel.trigger :error, cipher_error
63
64
  else
64
65
  raise cipher_error
@@ -1,3 +1,5 @@
1
+ require 'ably/modules/conversions'
2
+
1
3
  module Ably::Modules
2
4
  # Enum brings Enum like functionality used in other languages to Ruby
3
5
  #
@@ -62,7 +62,13 @@ module Ably
62
62
 
63
63
  # Trigger an event with event_name that will in turn call all matching callbacks setup with `on`
64
64
  def trigger(event_name, *args)
65
- callbacks[callbacks_event_coerced(event_name)].delete_if { |proc_hash| proc_hash[:trigger_proc].call(*args) }
65
+ callbacks[callbacks_event_coerced(event_name)].
66
+ clone.
67
+ select do |proc_hash|
68
+ proc_hash[:trigger_proc].call(*args)
69
+ end.each do |callback|
70
+ callbacks[callbacks_event_coerced(event_name)].delete callback
71
+ end
66
72
  end
67
73
 
68
74
  # Remove all callbacks for event_name.
@@ -1,3 +1,5 @@
1
+ require 'eventmachine'
2
+
1
3
  module Ably::Modules
2
4
  # EventMachineHelpers module provides common private methods to classes simplifying interaction with EventMachine
3
5
  module EventMachineHelpers
@@ -1,5 +1,7 @@
1
1
  require 'base64'
2
2
 
3
+ require 'ably/version'
4
+
3
5
  require 'ably/rest/middleware/encoder'
4
6
  require 'ably/rest/middleware/external_exceptions'
5
7
  require 'ably/rest/middleware/fail_if_unsupported_mime_type'
@@ -1,10 +1,12 @@
1
1
  require 'base64'
2
+ require 'ably/modules/conversions'
3
+ require 'ably/modules/message_pack'
2
4
 
3
5
  module Ably::Modules
4
6
  # Common model functionality shared across many {Ably::Models}
5
7
  module ModelCommon
6
- include Ably::Modules::Conversions
7
- include Ably::Modules::MessagePack
8
+ include Conversions
9
+ include MessagePack
8
10
 
9
11
  # Provide a normal Hash accessor to the underlying raw message object
10
12
  #
@@ -27,5 +29,13 @@ module Ably::Modules
27
29
  def to_json(*args)
28
30
  as_json.to_json(*args)
29
31
  end
32
+
33
+ private
34
+ def ensure_utf8_string_for(attribute, value)
35
+ if value
36
+ raise ArgumentError, "#{attribute} must be a String" unless value.kind_of?(String)
37
+ raise ArgumentError, "#{attribute} cannot use ASCII_8BIT encoding, please use UTF_8 encoding" unless value.encoding == Encoding::UTF_8
38
+ end
39
+ end
30
40
  end
31
41
  end
@@ -26,6 +26,7 @@ module Ably::Modules
26
26
  # connection.state = :invalid # raises an Exception as only a valid state can be defined
27
27
  # connection.trigger :invalid # raises an Exception as only a valid state can be used for EventEmitter
28
28
  # connection.change_state :connected # emits :connected event via EventEmitter, returns STATE.Connected
29
+ # connection.once_or_if(:connected) { puts 'block called once when state is connected or becomes connected' }
29
30
  #
30
31
  module StateEmitter
31
32
  # Current state {Ably::Modules::Enum}
@@ -55,7 +56,82 @@ module Ably::Modules
55
56
  end
56
57
  alias_method :change_state, :state=
57
58
 
59
+ # If the current state matches the target_state argument the block is called immediately.
60
+ # Else the block is called once when the target_state is reached.
61
+ #
62
+ # If the option block :else is provided then if any state other than target_state is reached, the :else block is called,
63
+ # however only one of the blocks will ever be called
64
+ #
65
+ # @param [Symbol,Ably::Modules::Enum,Array] target_states a single state or array of states that once met, will fire the success block only once
66
+ # @param [Hash] options
67
+ # @option options [Proc] :else block called once the state has changed to anything but target_state
68
+ #
69
+ # @yield block is called if the state is matched immediately or once when the state is reached
70
+ #
71
+ # @return [void]
72
+ def once_or_if(target_states, options = {}, &success_block)
73
+ raise ArgumentError, 'Block is expected' unless block_given?
74
+
75
+ if Array(target_states).any? { |target_state| state == target_state }
76
+ success_block.call
77
+ else
78
+ failure_block = options.fetch(:else, nil)
79
+ failure_wrapper = nil
80
+
81
+ success_wrapper = Proc.new do
82
+ success_block.call
83
+ off &success_wrapper
84
+ off &failure_wrapper if failure_wrapper
85
+ end
86
+
87
+ failure_wrapper = proc do |*args|
88
+ failure_block.call *args
89
+ off &success_wrapper
90
+ off &failure_wrapper
91
+ end if failure_block
92
+
93
+ Array(target_states).each do |target_state|
94
+ once target_state, &success_wrapper
95
+
96
+ once_state_changed do |*args|
97
+ failure_wrapper.call *args unless state == target_state
98
+ end if failure_block
99
+ end
100
+ end
101
+ end
102
+
103
+ # Calls the block once when the state changes
104
+ #
105
+ # @yield block is called once the state changes
106
+ # @return [void]
107
+ #
108
+ # @api private
109
+ def once_state_changed(&block)
110
+ raise ArgumentError, 'Block is expected' unless block_given?
111
+
112
+ once_block = proc do |*args|
113
+ off *self.class::STATE.map, &once_block
114
+ yield *args
115
+ end
116
+
117
+ once *self.class::STATE.map, &once_block
118
+ end
119
+
58
120
  private
121
+
122
+ # Returns an {EventMachine::Deferrable} and once the target state is reached, the
123
+ # success_block if provided and {EventMachine::Deferrable#callback} is called.
124
+ # If the state changes to any other state, the {EventMachine::Deferrable#errback} is called.
125
+ #
126
+ def deferrable_for_state_change_to(target_state, &success_block)
127
+ EventMachine::DefaultDeferrable.new.tap do |deferrable|
128
+ once_or_if(target_state, else: proc { |*args| deferrable.fail self, *args }) do
129
+ success_block.call self if block_given?
130
+ deferrable.succeed self
131
+ end
132
+ end
133
+ end
134
+
59
135
  def self.included(klass)
60
136
  klass.configure_event_emitter coerce_into: Proc.new { |event|
61
137
  if event == :error