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,153 @@
1
+ module Ably::Modules
2
+ # Conversions module provides common timestamp and variable naming conversions to Ably classes.
3
+ # All methods are private
4
+ module Conversions
5
+ extend self
6
+
7
+ private
8
+ def as_since_epoch(time, options = {})
9
+ return nil if options[:allow_nil] && !time
10
+
11
+ granularity = options.fetch(:granularity, :ms)
12
+
13
+ case time
14
+ when Time
15
+ time.to_f * multiplier_from_granularity(granularity)
16
+ when Numeric
17
+ time
18
+ else
19
+ raise ArgumentError, 'time argument must be a Numeric or Time object'
20
+ end.to_i
21
+ end
22
+
23
+ def as_time_from_epoch(time, options = {})
24
+ return nil if options[:allow_nil] && !time
25
+
26
+ granularity = options.fetch(:granularity, :ms)
27
+
28
+ case time
29
+ when Numeric
30
+ Time.at(time / multiplier_from_granularity(granularity))
31
+ when Time
32
+ time
33
+ else
34
+ raise ArgumentError, 'time argument must be a Numeric or Time object'
35
+ end
36
+ end
37
+
38
+ def multiplier_from_granularity(granularity)
39
+ case granularity
40
+ when :ms # milliseconds
41
+ 1_000.0
42
+ when :s # seconds
43
+ 1.0
44
+ else
45
+ raise ArgumentError, 'invalid granularity'
46
+ end
47
+ end
48
+
49
+ # Convert key to mixedCase from mixed_case
50
+ def convert_to_mixed_case(key, options = {})
51
+ force_camel = options.fetch(:force_camel, false)
52
+
53
+ key.to_s.
54
+ split('_').
55
+ each_with_index.map do |str, index|
56
+ if index > 0 || force_camel
57
+ str.capitalize
58
+ else
59
+ str
60
+ end
61
+ end.
62
+ join
63
+ end
64
+
65
+ # Convert a Hash into a mixed case Hash objet
66
+ # i.e. { client_id: 1 } becomes { 'clientId' => 1 }
67
+ def convert_to_mixed_case_hash(hash, options = {})
68
+ raise ArgumentError, 'Hash expected' unless hash.kind_of?(Hash)
69
+ hash.each_with_object({}) do |pair, new_hash|
70
+ key, val = pair
71
+ new_hash[convert_to_mixed_case(key, options)] = val
72
+ end
73
+ end
74
+
75
+ # Convert key to :snake_case from snakeCase
76
+ def convert_to_snake_case_symbol(key)
77
+ key.to_s.gsub(/::/, '/').
78
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
79
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
80
+ gsub(/([a-zA-Z])(\d)/,'\1_\2').
81
+ tr("-", "_").
82
+ downcase.
83
+ to_sym
84
+ end
85
+
86
+ def convert_to_lower_case(key)
87
+ key.to_s.gsub('_', '')
88
+ end
89
+
90
+ # Ensures that the string value is converted to UTF-8 encoding
91
+ # Unless option allow_nil: true, an {ArgumentError} is raised if the string_value is not a string
92
+ #
93
+ # @return <void>
94
+ #
95
+ def ensure_utf_8(field_name, string_value, options = {})
96
+ unless options[:allow_nil] && string_value.nil?
97
+ raise ArgumentError, "#{field_name} must be a String" unless string_value.kind_of?(String)
98
+ end
99
+ string_value.encode(Encoding::UTF_8) if string_value
100
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
101
+ raise ArgumentError, "#{field_name} could not be converted to UTF-8: #{e.message}"
102
+ end
103
+
104
+ # Ensures that the payload is a type supported by all Ably client libraries.
105
+ # Ably guarantees interoperability between client libraries and subsequently
106
+ # client libraries must reject unsupported types
107
+ #
108
+ # @return <void>
109
+ #
110
+ def ensure_supported_payload(payload)
111
+ return if payload.kind_of?(String) ||
112
+ payload.kind_of?(Hash) ||
113
+ payload.kind_of?(Array) ||
114
+ payload.nil?
115
+
116
+ raise Ably::Exceptions::UnsupportedDataType.new('Invalid data payload', 400, Ably::Exceptions::Codes::INVALID_MESSAGE_DATA_OR_ENCODING)
117
+ end
118
+
119
+ # Converts the name, data, attributes into the array of Message objects
120
+ #
121
+ # @return [Array<Ably::Models::Message>]
122
+ #
123
+ def build_messages(name, data = nil, attributes = {})
124
+ return [Ably::Models::Message(ensure_supported_name_and_payload(nil, data, attributes))] if name.nil?
125
+
126
+ Array(name).map do |item|
127
+ Ably::Models::Message(ensure_supported_name_and_payload(item, data, attributes))
128
+ end
129
+ end
130
+
131
+ # Ensures if the first argument (name) is a String, Hash or Ably::Models::Message object,
132
+ # second argument (data) should be a String, Hash, Array or nil (see ensure_supported_payload() method).
133
+ #
134
+ # @return [Hash] Contains :name, :data and other attributes
135
+ #
136
+ # (RSL1a, RSL1b)
137
+ #
138
+ def ensure_supported_name_and_payload(name, data = nil, attributes = {})
139
+ return name.attributes.dup if name.kind_of?(Ably::Models::Message)
140
+
141
+ payload = data
142
+ if (hash = name).kind_of?(Hash)
143
+ name, payload = hash[:name], (hash[:data] || payload)
144
+ attributes.merge!(hash)
145
+ end
146
+
147
+ name = ensure_utf_8(:name, name, allow_nil: true)
148
+ ensure_supported_payload payload
149
+
150
+ attributes.merge({ name: name, data: payload })
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,111 @@
1
+ require 'base64'
2
+ require 'ably/exceptions'
3
+
4
+ module Ably::Modules
5
+ # Provides methods to allow this model's `data` property to be encoded and decoded based on the `encoding` property.
6
+ #
7
+ # This module expects the following:
8
+ # - A #attributes method that returns the underlying hash object
9
+ # - A #set_attributes_object(attributes) method that updates the underlying hash object
10
+ # - A #raw_hash_object attribute that returns the original hash object used to create this object
11
+ #
12
+ module Encodeable
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ module ClassMethods
18
+ # Return a Message or Presence object from the encoded JSON-like object, using the optional channel options
19
+ # @param message_object [Hash] JSON-like object representation of an encoded message
20
+ # @param channel_options [Hash] Channel options, currently reserved for Encryption options
21
+ # @yield [Ably::Exceptions::BaseAblyException] yields an Ably exception if decoding fails
22
+ # @return [Message,Presence]
23
+ def from_encoded(message_object, channel_options = {}, &error_block)
24
+ new(message_object).tap do |message|
25
+ message.decode(encoders, channel_options, &error_block)
26
+ end
27
+ end
28
+
29
+ # Return an Array of Message or Presence objects from the encoded Array of JSON-like objects, using the optional channel options
30
+ # @param message_object_array [Array<Hash>] Array of JSON-like objects with encoded messages
31
+ # @param channel_options [Hash] Channel options, currently reserved for Encryption options
32
+ # @return [Array<Message,Presence>]
33
+ def from_encoded_array(message_object_array, channel_options = {})
34
+ Array(message_object_array).map do |message_object|
35
+ from_encoded(message_object, channel_options)
36
+ end
37
+ end
38
+
39
+ # Register an encoder for this object
40
+ # @api private
41
+ def register_encoder(encoder, options = {})
42
+ encoders << Ably::Models::MessageEncoders.encoder_from(encoder, options)
43
+ end
44
+
45
+ private
46
+ def encoders
47
+ @encoders ||= []
48
+ end
49
+ end
50
+
51
+ # Encode a message using the channel options and register encoders for the client
52
+ # @param encoders [Array<Ably::Models::MessageEncoders::Base>] List of encoders to apply to the message
53
+ # @param channel_options [Hash] Channel options, currently reserved for Encryption options
54
+ # @return [void]
55
+ # @api private
56
+ def encode(encoders, channel_options, &error_block)
57
+ apply_encoders :encode, encoders, channel_options, &error_block
58
+ end
59
+
60
+ # Decode a message using the channel options and registered encoders for the client
61
+ # @param encoders [Array<Ably::Models::MessageEncoders::Base>] List of encoders to apply to the message
62
+ # @param channel_options [Hash] Channel options, currently reserved for Encryption options
63
+ # @return [void]
64
+ # @api private
65
+ def decode(encoders, channel_options, &error_block)
66
+ apply_encoders :decode, encoders, channel_options, &error_block
67
+ end
68
+
69
+ # The original encoding of this message when it was received as a raw message from the Ably service
70
+ # @return [String,nil]
71
+ # @api private
72
+ def original_encoding
73
+ raw_hash_object['encoding']
74
+ end
75
+
76
+ private
77
+ def decode_binary_data_before_to_json(message)
78
+ data_key = message[:data] ? :data : 'data'
79
+ encoding_key = message[:encoding] ? :encoding : 'encoding'
80
+
81
+ if message[data_key].kind_of?(String) && message[data_key].encoding == ::Encoding::ASCII_8BIT
82
+ message[data_key] = ::Base64.encode64(message[data_key])
83
+ message[encoding_key] = [message[encoding_key], 'base64'].compact.join('/')
84
+ end
85
+ end
86
+
87
+ def apply_encoders(method, encoders, channel_options, &error_callback)
88
+ max_encoding_length = 512
89
+ message_attributes = attributes.dup
90
+
91
+ begin
92
+ if message_attributes[:encoding].to_s.length > max_encoding_length
93
+ raise Ably::Exceptions::EncoderError("Encoding error, encoding value is too long: '#{message_attributes[:encoding]}'", nil, 92100)
94
+ end
95
+
96
+ previous_encoding = message_attributes[:encoding]
97
+ encoders.each do |encoder|
98
+ encoder.public_send method, message_attributes, channel_options
99
+ end
100
+ end until previous_encoding == message_attributes[:encoding]
101
+
102
+ set_attributes_object message_attributes
103
+ rescue Ably::Exceptions::CipherError => cipher_error
104
+ if block_given?
105
+ yield cipher_error, "Encoder error #{cipher_error.code} trying to #{method} message: #{cipher_error.message}"
106
+ else
107
+ raise cipher_error
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,230 @@
1
+ require 'ably/modules/conversions'
2
+
3
+ module Ably::Modules
4
+ # Enum brings Enum like functionality used in other languages to Ruby
5
+ #
6
+ # @example
7
+ # class House
8
+ # extend Ably::Moduels::Enum
9
+ # CONSTRUCTION = ruby_enum('CONSTRUCTION',
10
+ # :brick,
11
+ # :steel,
12
+ # :wood
13
+ # )
14
+ # end
15
+ #
16
+ # House::CONSTRUCTION(:brick).to_i # => 0
17
+ # House::CONSTRUCTION('Wood').to_i # => 2
18
+ # House::CONSTRUCTION.Wood == :wood # => true
19
+ #
20
+ module Enum
21
+ private
22
+
23
+ class Base; end
24
+
25
+ # ruby_enum returns an Enum-like class that should be assigned to a constant in your class
26
+ # The first `enum_name` argument must match the constant name so that the coercion method is available
27
+ #
28
+ # @example
29
+ # class House
30
+ # extend Ably::Moduels::Enum
31
+ # CONSTRUCTION = ruby_enum('CONSTRUCTION', :brick)
32
+ # end
33
+ #
34
+ # # ensures the following coercion method is available
35
+ # House::CONSTRUCTION(:brick) # => CONSTRUCTION.Brick
36
+ #
37
+ def ruby_enum(enum_name, *values)
38
+ enum_class = Class.new(Enum::Base) do
39
+ include Conversions
40
+ extend Conversions
41
+
42
+ @enum_name = enum_name
43
+ @by_index = {}
44
+ @by_symbol = {}
45
+
46
+ class << self
47
+ include Enumerable
48
+
49
+ def get(identifier)
50
+ case identifier
51
+ when Symbol
52
+ by_symbol.fetch(identifier) { raise KeyError, "#{name} key not found: :#{identifier}" }
53
+ when String
54
+ by_symbol.fetch(convert_to_snake_case_symbol(identifier)) { raise KeyError, "#{name} key not found: '#{identifier}'" }
55
+ when Numeric
56
+ by_index.fetch(identifier) { raise KeyError, "#{name} key not found: #{identifier}" }
57
+ when ancestors.first
58
+ identifier
59
+ else
60
+ if identifier.class.ancestors.include?(Enum::Base)
61
+ by_symbol.fetch(identifier.to_sym)
62
+ else
63
+ raise KeyError, "Cannot find Enum matching identifier '#{identifier}' argument as it is an unacceptable type: #{identifier.class}"
64
+ end
65
+ end
66
+ end
67
+
68
+ def [](*args)
69
+ get(*args)
70
+ end
71
+
72
+ def to_s
73
+ name
74
+ end
75
+
76
+ def size
77
+ by_symbol.keys.length
78
+ end
79
+ alias_method :length, :size
80
+
81
+ # Method ensuring this {Enum} is {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
82
+ def each(&block)
83
+ return to_enum(:each) unless block_given?
84
+ by_symbol.values.each(&block)
85
+ end
86
+
87
+ # The name provided in the constructor for this Enum
88
+ def name
89
+ @enum_name
90
+ end
91
+
92
+ # Array of Enum values as symbols
93
+ #
94
+ # @return [Array<Symbol>]
95
+ #
96
+ def to_sym_arr
97
+ @by_symbol.keys
98
+ end
99
+
100
+ private
101
+ def by_index
102
+ @by_index
103
+ end
104
+
105
+ def by_symbol
106
+ @by_symbol
107
+ end
108
+
109
+ # Define constants for each of the Enum values
110
+ # e.g. define_constants(:dog) creates Enum::Dog
111
+ def define_values(values)
112
+ raise RuntimeError, "#{name} Enum cannot be modified" if by_index.frozen?
113
+
114
+ # Allow another Enum to be used as a set of values
115
+ if values.length == 1 && klass = values.first
116
+ if klass.kind_of?(Class) && klass.ancestors.include?(Enum::Base)
117
+ values = values.first.map(&:to_sym)
118
+ end
119
+ end
120
+
121
+ values.map do |value|
122
+ # Convert any key => index_value pairs into array pairs
123
+ Array(value)
124
+ end.flatten(1).each_with_index do |name, index|
125
+ name, index = name if name.kind_of?(Array) # name is key => index_value pair
126
+ raise ArgumentError, "Index value '#{index}' is invalid" unless index.kind_of?(Numeric)
127
+
128
+ camel_name = convert_to_mixed_case(name, force_camel: true)
129
+ name_symbol = convert_to_snake_case_symbol(name)
130
+ enum = new(camel_name, name_symbol, index.to_i)
131
+
132
+ by_index[index.to_i] = enum
133
+ by_symbol[name_symbol] = enum
134
+
135
+ define_singleton_method camel_name do
136
+ enum
137
+ end
138
+ end
139
+
140
+ by_index.freeze
141
+ by_symbol.freeze
142
+ end
143
+ end
144
+
145
+ def initialize(name, symbol, index)
146
+ @name = name
147
+ @index = index
148
+ @symbol = symbol
149
+ end
150
+
151
+ def to_s
152
+ "#{self.class}.#{name}"
153
+ end
154
+
155
+ def to_i
156
+ index
157
+ end
158
+
159
+ def to_sym
160
+ symbol
161
+ end
162
+
163
+ def to_json(*args)
164
+ %{"#{symbol}"}
165
+ end
166
+
167
+ # Allow comparison of Enum objects based on:
168
+ #
169
+ # * Other equivalent Enum objects compared by Symbol (not Integer value)
170
+ # * Symbol
171
+ # * String
172
+ # * Integer index of Enum
173
+ #
174
+ def ==(other)
175
+ case other
176
+ when Symbol
177
+ self.to_sym == convert_to_snake_case_symbol(other)
178
+ when String
179
+ self.to_sym == convert_to_snake_case_symbol(other)
180
+ when Numeric
181
+ self.to_i == other.to_i
182
+ else
183
+ if other.kind_of?(Ably::Modules::Enum::Base)
184
+ self.to_sym == other.to_sym
185
+ end
186
+ end
187
+ end
188
+
189
+ def match_any?(*enums)
190
+ enums.any? { |enum| self.==(enum) }
191
+ end
192
+
193
+ private
194
+ def name
195
+ @name
196
+ end
197
+
198
+ def index
199
+ @index
200
+ end
201
+
202
+ def symbol
203
+ @symbol
204
+ end
205
+
206
+ define_values values
207
+ end
208
+
209
+ # Convert any comparable object into this Enum
210
+ # @example
211
+ # class Example
212
+ # DOGS = ruby_enum('DOGS', :terrier, :labrador, :great_dane)
213
+ # end
214
+ #
215
+ # Example.DOGS(:great_dane) # => <DOGS.GreatDane>
216
+ # Example.DOGS(0) # => <DOGS.Terrier>
217
+ # Example.new.DOGS(0) # => <DOGS.Terrier>
218
+ #
219
+ define_singleton_method enum_name do |val|
220
+ enum_class.get(val)
221
+ end
222
+
223
+ define_method enum_name do |val|
224
+ enum_class.get(val)
225
+ end
226
+
227
+ enum_class
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,200 @@
1
+ require 'ably/modules/safe_yield'
2
+
3
+ module Ably
4
+ module Modules
5
+ # EventEmitter provides methods to attach to public events and emit events on any class instance
6
+ #
7
+ # EventEmitter are typically used for public interfaces, and as such, may be overriden in
8
+ # the classes to enforce `event` names match expected values.
9
+ #
10
+ # @note This module requires that the method #logger is defined.
11
+ #
12
+ # @example
13
+ # class Example
14
+ # include Modules::EventEmitter
15
+ # end
16
+ #
17
+ # event_emitter = Example.new
18
+ # event_emitter.on(:signal) { |name| puts "Signal #{name} received" }
19
+ # event_emitter.emit :signal, "Test"
20
+ # #=> "Signal Test received"
21
+ #
22
+ module EventEmitter
23
+ include Ably::Modules::SafeYield
24
+
25
+ module ClassMethods
26
+ attr_reader :event_emitter_coerce_proc
27
+
28
+ # Configure included EventEmitter
29
+ #
30
+ # @param [Hash] options the options for the {EventEmitter}
31
+ # @option options [Proc] :coerce_into A lambda/Proc that is used to coerce the event names for all events. This is useful to ensure the event names conform to a naming or type convention.
32
+ #
33
+ # @example
34
+ # configure_event_emitter coerce_into: lambda { |event| event.to_sym }
35
+ #
36
+ def configure_event_emitter(options = {})
37
+ @event_emitter_coerce_proc = options[:coerce_into]
38
+ end
39
+
40
+ # Ensure @event_emitter_coerce_proc option is passed down to any classes that inherit the class with callbacks
41
+ def inherited(subclass)
42
+ subclass.instance_variable_set('@event_emitter_coerce_proc', @event_emitter_coerce_proc) if defined?(@event_emitter_coerce_proc)
43
+ super
44
+ end
45
+ end
46
+
47
+ # On receiving an event matching the event_name, call the provided block
48
+ #
49
+ # @spec RTE4
50
+ #
51
+ # @param [Array<String>] event_names event name
52
+ #
53
+ # @return [void]
54
+ #
55
+ def on(*event_names, &block)
56
+ add_callback event_names, proc_for_block(block)
57
+ end
58
+
59
+ # Equivalent of {#on} but any exception raised in a block will bubble up and cause this client library to fail.
60
+ # This method is designed to be used internally by the client library.
61
+ # @api private
62
+ def unsafe_on(*event_names, &block)
63
+ add_callback event_names, proc_for_block(block, unsafe: true)
64
+ end
65
+
66
+ # On receiving an event maching the event_name, call the provided block only once and remove the registered callback
67
+ #
68
+ # @spec RTE4
69
+ #
70
+ # @param [Array<String>] event_names event name
71
+ #
72
+ # @return [void]
73
+ #
74
+ def once(*event_names, &block)
75
+ add_callback event_names, proc_for_block(block, delete_once_run: true)
76
+ end
77
+
78
+ # Equivalent of {#once} but any exception raised in a block will bubble up and cause this client library to fail.
79
+ # This method is designed to be used internally by the client library.
80
+ # @api private
81
+ def unsafe_once(*event_names, &block)
82
+ add_callback event_names, proc_for_block(block, delete_once_run: true, unsafe: true)
83
+ end
84
+
85
+ # Emits an event, calling registered listeners with the given event name and any other given arguments.
86
+ # If an exception is raised in any of the listeners, the exception is caught by the EventEmitter and the exception is logged to the Ably logger.
87
+ #
88
+ # @spec RTE6
89
+ #
90
+ def emit(event_name, *args)
91
+ [callbacks_any, callbacks[callbacks_event_coerced(event_name)]].each do |callback_arr|
92
+ callback_arr.clone.
93
+ select do |proc_hash|
94
+ if proc_hash[:unsafe]
95
+ proc_hash[:emit_proc].call(*args)
96
+ else
97
+ safe_yield proc_hash[:emit_proc], *args
98
+ end
99
+ end.each do |callback|
100
+ callback_arr.delete callback
101
+ end
102
+ end
103
+ end
104
+
105
+ # Remove all callbacks for event_name.
106
+ #
107
+ # If a block is provided, only callbacks matching that block signature will be removed.
108
+ # If block is not provided, all callbacks matching the event_name will be removed.
109
+ #
110
+ # @spec RTE5
111
+ #
112
+ # @param [Array<String>] event_names event name
113
+ #
114
+ # @return [void]
115
+ #
116
+ def off(*event_names, &block)
117
+ off_internal(false, *event_names, &block)
118
+ end
119
+
120
+ # Equivalent of {#off} but only unsafe listeners are removed.
121
+ # This method is designed to be used internally by the client library.
122
+ # @api private
123
+ def unsafe_off(*event_names, &block)
124
+ off_internal(true, *event_names, &block)
125
+ end
126
+
127
+ private
128
+ def off_internal(unsafe, *event_names, &block)
129
+ keys = if event_names.empty?
130
+ callbacks.keys
131
+ else
132
+ event_names
133
+ end
134
+
135
+ if event_names.empty?
136
+ callbacks_any.delete_if do |proc_hash|
137
+ if block_given?
138
+ (proc_hash[:unsafe] == unsafe) && (proc_hash[:block] == block)
139
+ else
140
+ proc_hash[:unsafe] == unsafe
141
+ end
142
+ end
143
+ end
144
+
145
+ keys.each do |event_name|
146
+ callbacks[callbacks_event_coerced(event_name)].delete_if do |proc_hash|
147
+ if block_given?
148
+ (proc_hash[:unsafe] == unsafe) && (proc_hash[:block] == block)
149
+ else
150
+ proc_hash[:unsafe] == unsafe
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ def self.included(klass)
157
+ klass.extend ClassMethods
158
+ end
159
+
160
+ def add_callback(event_names, proc_block)
161
+ if event_names.empty?
162
+ callbacks_any << proc_block
163
+ else
164
+ event_names.each do |event_name|
165
+ callbacks[callbacks_event_coerced(event_name)] << proc_block
166
+ end
167
+ end
168
+ end
169
+
170
+ # Create a Hash with a proc that calls the provided block and returns true if option :delete_once_run is set to true.
171
+ # #emit automatically deletes any blocks that return true thus allowing a block to be run once
172
+ def proc_for_block(block, options = {})
173
+ {
174
+ emit_proc: lambda do |*args|
175
+ block.call(*args)
176
+ true if options[:delete_once_run]
177
+ end,
178
+ block: block,
179
+ unsafe: options[:unsafe] || false
180
+ }
181
+ end
182
+
183
+ def callbacks
184
+ @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
185
+ end
186
+
187
+ def callbacks_any
188
+ @callbacks_any ||= []
189
+ end
190
+
191
+ def callbacks_event_coerced(event_name)
192
+ if self.class.event_emitter_coerce_proc
193
+ self.class.event_emitter_coerce_proc.call(event_name)
194
+ else
195
+ event_name
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end