ably 0.7.2 → 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/LICENSE.txt +1 -1
  2. data/README.md +107 -24
  3. data/SPEC.md +531 -398
  4. data/lib/ably/auth.rb +23 -15
  5. data/lib/ably/exceptions.rb +9 -0
  6. data/lib/ably/models/message.rb +17 -9
  7. data/lib/ably/models/paginated_resource.rb +12 -8
  8. data/lib/ably/models/presence_message.rb +18 -10
  9. data/lib/ably/models/protocol_message.rb +15 -4
  10. data/lib/ably/modules/async_wrapper.rb +4 -3
  11. data/lib/ably/modules/event_emitter.rb +31 -2
  12. data/lib/ably/modules/message_emitter.rb +77 -0
  13. data/lib/ably/modules/safe_deferrable.rb +71 -0
  14. data/lib/ably/modules/safe_yield.rb +41 -0
  15. data/lib/ably/modules/state_emitter.rb +28 -8
  16. data/lib/ably/realtime.rb +0 -5
  17. data/lib/ably/realtime/channel.rb +24 -29
  18. data/lib/ably/realtime/channel/channel_manager.rb +54 -11
  19. data/lib/ably/realtime/channel/channel_state_machine.rb +21 -6
  20. data/lib/ably/realtime/client.rb +7 -2
  21. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +29 -26
  22. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +4 -4
  23. data/lib/ably/realtime/connection.rb +41 -9
  24. data/lib/ably/realtime/connection/connection_manager.rb +72 -24
  25. data/lib/ably/realtime/connection/connection_state_machine.rb +26 -4
  26. data/lib/ably/realtime/connection/websocket_transport.rb +19 -6
  27. data/lib/ably/realtime/presence.rb +74 -208
  28. data/lib/ably/realtime/presence/members_map.rb +264 -0
  29. data/lib/ably/realtime/presence/presence_manager.rb +59 -0
  30. data/lib/ably/realtime/presence/presence_state_machine.rb +64 -0
  31. data/lib/ably/rest/channel.rb +1 -1
  32. data/lib/ably/rest/client.rb +6 -2
  33. data/lib/ably/rest/presence.rb +1 -1
  34. data/lib/ably/util/pub_sub.rb +3 -1
  35. data/lib/ably/util/safe_deferrable.rb +18 -0
  36. data/lib/ably/version.rb +1 -1
  37. data/spec/acceptance/realtime/channel_history_spec.rb +2 -2
  38. data/spec/acceptance/realtime/channel_spec.rb +28 -6
  39. data/spec/acceptance/realtime/connection_failures_spec.rb +116 -46
  40. data/spec/acceptance/realtime/connection_spec.rb +55 -10
  41. data/spec/acceptance/realtime/message_spec.rb +32 -0
  42. data/spec/acceptance/realtime/presence_spec.rb +456 -96
  43. data/spec/acceptance/realtime/stats_spec.rb +2 -2
  44. data/spec/acceptance/realtime/time_spec.rb +2 -2
  45. data/spec/acceptance/rest/auth_spec.rb +75 -7
  46. data/spec/shared/client_initializer_behaviour.rb +8 -0
  47. data/spec/shared/safe_deferrable_behaviour.rb +71 -0
  48. data/spec/support/api_helper.rb +1 -1
  49. data/spec/support/event_machine_helper.rb +1 -1
  50. data/spec/support/test_app.rb +13 -7
  51. data/spec/unit/models/message_spec.rb +15 -14
  52. data/spec/unit/models/paginated_resource_spec.rb +4 -4
  53. data/spec/unit/models/presence_message_spec.rb +17 -17
  54. data/spec/unit/models/stat_spec.rb +4 -4
  55. data/spec/unit/modules/async_wrapper_spec.rb +28 -9
  56. data/spec/unit/modules/event_emitter_spec.rb +50 -0
  57. data/spec/unit/modules/state_emitter_spec.rb +76 -2
  58. data/spec/unit/realtime/channel_spec.rb +51 -20
  59. data/spec/unit/realtime/channels_spec.rb +3 -3
  60. data/spec/unit/realtime/connection_spec.rb +30 -0
  61. data/spec/unit/realtime/presence_spec.rb +52 -26
  62. data/spec/unit/realtime/safe_deferrable_spec.rb +12 -0
  63. metadata +85 -39
  64. checksums.yaml +0 -7
  65. data/.ruby-version.old +0 -1
data/lib/ably/auth.rb CHANGED
@@ -34,7 +34,7 @@ module Ably
34
34
  # Creates an Auth object
35
35
  #
36
36
  # @param [Ably::Rest::Client] client {Ably::Rest::Client} this Auth object uses
37
- # @param options (see Ably::Rest::Client#initialize)
37
+ # @param [Hash] options (see Ably::Rest::Client#initialize)
38
38
  # @option (see Ably::Rest::Client#initialize)
39
39
  # @yield (see Ably::Rest::Client#initialize)
40
40
  #
@@ -53,12 +53,7 @@ module Ably
53
53
  raise ArgumentError, 'api_key and key_id or key_secret are mutually exclusive. Provider either an api_key or key_id & key_secret'
54
54
  end
55
55
 
56
- if auth_options[:api_key]
57
- api_key_parts = auth_options[:api_key].to_s.match(/(?<id>[\w_-]+\.[\w_-]+):(?<secret>[\w_-]+)/)
58
- raise ArgumentError, 'api_key is invalid' unless api_key_parts
59
- auth_options[:key_id] = api_key_parts[:id].encode(Encoding::UTF_8)
60
- auth_options[:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8)
61
- end
56
+ split_api_key_into_key_and_secret! auth_options if auth_options[:api_key]
62
57
 
63
58
  if using_basic_auth? && !api_key_present?
64
59
  raise ArgumentError, 'api_key is missing. Either an API key, token, or token auth method must be provided'
@@ -78,6 +73,7 @@ module Ably
78
73
  #
79
74
  # @param [Hash] options the options for the token request
80
75
  # @option options (see #request_token)
76
+ # @option options [String] :api_key API key comprising the key ID and key secret in a single string
81
77
  # @option options [Boolean] :force obtains a new token even if the current token is valid
82
78
  #
83
79
  # @yield (see #request_token)
@@ -102,6 +98,9 @@ module Ably
102
98
  return current_token unless current_token.expired?
103
99
  end
104
100
 
101
+ options = options.clone
102
+ split_api_key_into_key_and_secret! options if options[:api_key]
103
+
105
104
  @options = @options.merge(options)
106
105
  @default_token_block = token_request_block if block_given?
107
106
 
@@ -157,10 +156,13 @@ module Ably
157
156
 
158
157
  token_request = IdiomaticRubyWrapper(token_request)
159
158
 
160
- response = client.post("/keys/#{token_request.fetch(:id)}/requestToken", token_request.hash, send_auth_header: false, disable_automatic_reauthorise: true)
161
- body = IdiomaticRubyWrapper(response.body)
162
-
163
- Ably::Models::Token.new(body.fetch(:access_token))
159
+ if token_request.has_key?(:issued_at) && token_request.has_key?(:expires)
160
+ Ably::Models::Token.new(token_request)
161
+ else
162
+ response = client.post("/keys/#{token_request.fetch(:id)}/requestToken", token_request.hash, send_auth_header: false, disable_automatic_reauthorise: true)
163
+ body = IdiomaticRubyWrapper(response.body)
164
+ Ably::Models::Token.new(body.fetch(:access_token))
165
+ end
164
166
  end
165
167
 
166
168
  # Creates and signs a token request that can then subsequently be used by any client to request a token
@@ -191,6 +193,7 @@ module Ably
191
193
  token_attributes = %w(id client_id ttl timestamp capability nonce)
192
194
 
193
195
  token_options = options.clone
196
+
194
197
  request_key_id = token_options.delete(:key_id) || key_id
195
198
  request_key_secret = token_options.delete(:key_secret) || key_secret
196
199
 
@@ -296,7 +299,7 @@ module Ably
296
299
  end
297
300
 
298
301
  private
299
- attr_reader :default_token_block
302
+ attr_reader :client, :default_token_block
300
303
 
301
304
  def ensure_api_key_sent_over_secure_connection
302
305
  raise Ably::Exceptions::InsecureRequestError, 'Cannot use Basic Auth over non-TLS connections' unless authentication_security_requirements_met?
@@ -308,6 +311,14 @@ module Ably
308
311
  "Basic #{encode64("#{api_key}")}"
309
312
  end
310
313
 
314
+ def split_api_key_into_key_and_secret!(options)
315
+ api_key_parts = options[:api_key].to_s.match(/(?<id>[\w_-]+\.[\w_-]+):(?<secret>[\w_-]+)/)
316
+ raise ArgumentError, 'api_key is invalid' unless api_key_parts
317
+
318
+ options[:key_id] = api_key_parts[:id].encode(Encoding::UTF_8)
319
+ options[:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8)
320
+ end
321
+
311
322
  def token_auth_id
312
323
  if token_id
313
324
  token_id
@@ -431,8 +442,5 @@ module Ably
431
442
  def api_key_present?
432
443
  key_id && key_secret
433
444
  end
434
-
435
- private
436
- attr_reader :client
437
445
  end
438
446
  end
@@ -44,9 +44,15 @@ module Ably
44
44
  # Connection Timeout accessing Realtime or REST service
45
45
  class ConnectionTimeoutError < ConnectionError; end
46
46
 
47
+ # Connection closed unexpectedly
48
+ class ConnectionClosedError < ConnectionError; end
49
+
47
50
  # Invalid State Change error on a {https://github.com/gocardless/statesman Statesman State Machine}
48
51
  class StateChangeError < BaseAblyException; end
49
52
 
53
+ # The state of the object is not suitable for this operation
54
+ class IncompatibleStateForOperation < BaseAblyException; end
55
+
50
56
  # A generic Ably exception taht supports a status & code.
51
57
  # See https://github.com/ably/ably-common/blob/master/protocol/errors.json for a list of Ably errors
52
58
  class Standard < BaseAblyException; end
@@ -65,5 +71,8 @@ module Ably
65
71
 
66
72
  # The token request could not be created
67
73
  class TokenRequestError < BaseAblyException; end
74
+
75
+ # The message could not be delivered to the server
76
+ class MessageDeliveryError < BaseAblyException; end
68
77
  end
69
78
  end
@@ -2,17 +2,17 @@ module Ably::Models
2
2
  # Convert messsage argument to a {Message} object and associate with a protocol message if provided
3
3
  #
4
4
  # @param message [Message,Hash] A message object or Hash of message properties
5
- # @param protocol_message [ProtocolMessage] An optional protocol message to assocate the message with
5
+ # @param [Hash] options (see Message#initialize)
6
6
  #
7
7
  # @return [Message]
8
- def self.Message(message, protocol_message = nil)
8
+ def self.Message(message, options = {})
9
9
  case message
10
10
  when Message
11
11
  message.tap do
12
- message.assign_to_protocol_message protocol_message
12
+ message.assign_to_protocol_message options[:protocol_message] if options[:protocol_message]
13
13
  end
14
14
  else
15
- Message.new(message, protocol_message)
15
+ Message.new(message, options)
16
16
  end
17
17
  end
18
18
 
@@ -41,15 +41,18 @@ module Ably::Models
41
41
  include Ably::Modules::Conversions
42
42
  include Ably::Modules::Encodeable
43
43
  include Ably::Modules::ModelCommon
44
- include EventMachine::Deferrable
44
+ include Ably::Modules::SafeDeferrable if defined?(Ably::Realtime)
45
45
 
46
46
  # {Message} initializer
47
47
  #
48
- # @param hash_object [Hash] object with the underlying message details
49
- # @param protocol_message [ProtocolMessage] if this message has been published, then it is associated with a {ProtocolMessage}
48
+ # @param hash_object [Hash] object with the underlying message details
49
+ # @param [Hash] options an options Hash for this initializer
50
+ # @option options [ProtocolMessage] :protocol_message An optional protocol message to assocate the presence message with
51
+ # @option options [Logger] :logger An optional Logger to be used by {Ably::Modules::SafeDeferrable} if an exception is caught in a callback
50
52
  #
51
- def initialize(hash_object, protocol_message = nil)
52
- @protocol_message = protocol_message
53
+ def initialize(hash_object, options = {})
54
+ @logger = options[:logger] # Logger expected for SafeDeferrable
55
+ @protocol_message = options[:protocol_message]
53
56
  @raw_hash_object = hash_object
54
57
 
55
58
  set_hash_object hash_object
@@ -128,5 +131,10 @@ module Ably::Models
128
131
  def set_hash_object(hash)
129
132
  @hash_object = IdiomaticRubyWrapper(hash.clone.freeze, stop_at: [:data])
130
133
  end
134
+
135
+ def logger
136
+ return logger if logger
137
+ protocol_message.logger if protocol_message
138
+ end
131
139
  end
132
140
  end
@@ -31,23 +31,23 @@ module Ably::Models
31
31
  end
32
32
 
33
33
  # Retrieve the first page of results.
34
- # When used as part of the {Ably::Realtime} library, it will return a {EventMachine::Deferrable} object,
34
+ # When used as part of the {Ably::Realtime} library, it will return a {Ably::Util::SafeDeferrable} object,
35
35
  # and allows an optional success callback block to be provided.
36
36
  #
37
- # @return [PaginatedResource,EventMachine::Deferrable]
37
+ # @return [PaginatedResource,Ably::Util::SafeDeferrable]
38
38
  def first_page(&success_callback)
39
- async_wrap_if(make_async, success_callback) do
39
+ async_wrap_if_realtime(success_callback) do
40
40
  PaginatedResource.new(client.get(pagination_url('first')), base_url, client, pagination_options, &each_block)
41
41
  end
42
42
  end
43
43
 
44
44
  # Retrieve the next page of results.
45
- # When used as part of the {Ably::Realtime} library, it will return a {EventMachine::Deferrable} object,
45
+ # When used as part of the {Ably::Realtime} library, it will return a {Ably::Util::SafeDeferrable} object,
46
46
  # and allows an optional success callback block to be provided.
47
47
  #
48
- # @return [PaginatedResource,EventMachine::Deferrable]
48
+ # @return [PaginatedResource,Ably::Util::SafeDeferrable]
49
49
  def next_page(&success_callback)
50
- async_wrap_if(make_async, success_callback) do
50
+ async_wrap_if_realtime(success_callback) do
51
51
  raise Ably::Exceptions::InvalidPageError, 'There are no more pages' if supports_pagination? && last_page?
52
52
  PaginatedResource.new(client.get(pagination_url('next')), base_url, client, pagination_options, &each_block)
53
53
  end
@@ -161,13 +161,17 @@ module Ably::Models
161
161
  }
162
162
  end
163
163
 
164
- def async_wrap_if(is_realtime, success_callback, &operation)
165
- if is_realtime
164
+ def async_wrap_if_realtime(success_callback, &operation)
165
+ if make_async
166
166
  raise 'EventMachine is required for asynchronous operations' unless defined?(EventMachine)
167
167
  async_wrap success_callback, &operation
168
168
  else
169
169
  yield
170
170
  end
171
171
  end
172
+
173
+ def logger
174
+ client.logger
175
+ end
172
176
  end
173
177
  end
@@ -2,17 +2,17 @@ module Ably::Models
2
2
  # Convert presence_messsage argument to a {PresenceMessage} object and associate with a protocol message if provided
3
3
  #
4
4
  # @param presence_message [PresenceMessage,Hash] A presence message object or Hash of presence message properties
5
- # @param protocol_message [ProtocolMessage] An optional protocol message to assocate the presence message with
5
+ # @param [Hash] options (see PresenceMessage#initialize)
6
6
  #
7
7
  # @return [PresenceMessage]
8
- def self.PresenceMessage(presence_message, protocol_message = nil)
8
+ def self.PresenceMessage(presence_message, options = {})
9
9
  case presence_message
10
10
  when PresenceMessage
11
11
  presence_message.tap do
12
- presence_message.assign_to_protocol_message protocol_message
12
+ presence_message.assign_to_protocol_message options[:protocol_message] if options[:protocol_message]
13
13
  end
14
14
  else
15
- PresenceMessage.new(presence_message, protocol_message)
15
+ PresenceMessage.new(presence_message, options)
16
16
  end
17
17
  end
18
18
 
@@ -41,7 +41,7 @@ module Ably::Models
41
41
  include Ably::Modules::Conversions
42
42
  include Ably::Modules::Encodeable
43
43
  include Ably::Modules::ModelCommon
44
- include EventMachine::Deferrable
44
+ include Ably::Modules::SafeDeferrable if defined?(Ably::Realtime)
45
45
  extend Ably::Modules::Enum
46
46
 
47
47
  ACTION = ruby_enum('ACTION',
@@ -52,13 +52,16 @@ module Ably::Models
52
52
  :update
53
53
  )
54
54
 
55
- # {Message} initializer
55
+ # {PresenceMessage} initializer
56
56
  #
57
- # @param hash_object [Hash] object with the underlying message details
58
- # @param protocol_message [ProtocolMessage] if this message has been published, then it is associated with a {ProtocolMessage}
57
+ # @param hash_object [Hash] object with the underlying presence message details
58
+ # @param [Hash] options an options Hash for this initializer
59
+ # @option options [ProtocolMessage] :protocol_message An optional protocol message to assocate the presence message with
60
+ # @option options [Logger] :logger An optional Logger to be used by {Ably::Modules::SafeDeferrable} if an exception is caught in a callback
59
61
  #
60
- def initialize(hash_object, protocol_message = nil)
61
- @protocol_message = protocol_message
62
+ def initialize(hash_object, options = {})
63
+ @logger = options[:logger] # Logger expected for SafeDeferrable
64
+ @protocol_message = options[:protocol_message]
62
65
  @raw_hash_object = hash_object
63
66
 
64
67
  set_hash_object hash_object
@@ -143,5 +146,10 @@ module Ably::Models
143
146
  def set_hash_object(hash)
144
147
  @hash_object = IdiomaticRubyWrapper(hash.clone.freeze, stop_at: [:data])
145
148
  end
149
+
150
+ def logger
151
+ return logger if logger
152
+ protocol_message.logger if protocol_message
153
+ end
146
154
  end
147
155
  end
@@ -37,7 +37,7 @@ module Ably::Models
37
37
  #
38
38
  class ProtocolMessage
39
39
  include Ably::Modules::ModelCommon
40
- include EventMachine::Deferrable if defined?(Ably::Realtime)
40
+ include Ably::Modules::SafeDeferrable if defined?(Ably::Realtime)
41
41
  extend Ably::Modules::Enum
42
42
 
43
43
  # Actions which are sent by the Ably Realtime API
@@ -69,7 +69,15 @@ module Ably::Models
69
69
  [ACTION.Presence, ACTION.Message].include?(ACTION(for_action))
70
70
  end
71
71
 
72
- def initialize(hash_object)
72
+ # {ProtocolMessage} initializer
73
+ #
74
+ # @param hash_object [Hash] object with the underlying protocol message data
75
+ # @param [Hash] options an options Hash for this initializer
76
+ # @option options [Logger] :logger An optional Logger to be used by {Ably::Modules::SafeDeferrable} if an exception is caught in a callback
77
+ #
78
+ def initialize(hash_object, options = {})
79
+ @logger = options[:logger] # Logger expected for SafeDeferrable
80
+
73
81
  @raw_hash_object = hash_object
74
82
  @hash_object = IdiomaticRubyWrapper(@raw_hash_object.clone)
75
83
 
@@ -147,7 +155,7 @@ module Ably::Models
147
155
  def messages
148
156
  @messages ||=
149
157
  Array(hash[:messages]).map do |message|
150
- Ably::Models.Message(message, self)
158
+ Ably::Models.Message(message, protocol_message: self)
151
159
  end
152
160
  end
153
161
 
@@ -158,7 +166,7 @@ module Ably::Models
158
166
  def presence
159
167
  @presence ||=
160
168
  Array(hash[:presence]).map do |message|
161
- Ably::Models.PresenceMessage(message, self)
169
+ Ably::Models.PresenceMessage(message, protocol_message: self)
162
170
  end
163
171
  end
164
172
 
@@ -206,5 +214,8 @@ module Ably::Models
206
214
  action_enum = action rescue nil
207
215
  !action_enum || (ack_required? && !has_serial?)
208
216
  end
217
+
218
+ private
219
+ attr_reader :logger
209
220
  end
210
221
  end
@@ -7,6 +7,7 @@ module Ably::Modules
7
7
  #
8
8
  # @note using this AsyncWrapper should only be used for methods that are used less frequently and typically
9
9
  # not run with levels of concurrency due to the limited number of threads available to EventMachine by default.
10
+ # This module requires that the method #logger is defined.
10
11
  #
11
12
  # @example
12
13
  # class BlockingOperation
@@ -32,15 +33,15 @@ module Ably::Modules
32
33
  module AsyncWrapper
33
34
  private
34
35
 
35
- # Will yield the provided block in a new thread and return an {EventMachine::Deferrable http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Deferrable}
36
+ # Will yield the provided block in a new thread and return an {Ably::Util::SafeDeferrable}
36
37
  #
37
38
  # @yield [Object] operation block that is run in a thread
38
- # @return [EventMachine::Deferrable]
39
+ # @return [Ably::Util::SafeDeferrable]
39
40
  #
40
41
  def async_wrap(success_callback = nil)
41
42
  raise ArgumentError, 'Block required' unless block_given?
42
43
 
43
- EventMachine::DefaultDeferrable.new.tap do |deferrable|
44
+ Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
44
45
  deferrable.callback &success_callback if success_callback
45
46
 
46
47
  operation_with_exception_handling = proc do
@@ -1,3 +1,5 @@
1
+ require 'ably/modules/safe_yield'
2
+
1
3
  module Ably
2
4
  module Modules
3
5
  # EventEmitter provides methods to attach to public events and trigger events on any class instance
@@ -5,6 +7,8 @@ module Ably
5
7
  # EventEmitter are typically used for public interfaces, and as such, may be overriden in
6
8
  # the classes to enforce `event` names match expected values.
7
9
  #
10
+ # @note This module requires that the method #logger is defined.
11
+ #
8
12
  # @example
9
13
  # class Example
10
14
  # include Modules::EventEmitter
@@ -16,6 +20,8 @@ module Ably
16
20
  # #=> "Signal Test received"
17
21
  #
18
22
  module EventEmitter
23
+ include Ably::Modules::SafeYield
24
+
19
25
  module ClassMethods
20
26
  attr_reader :event_emitter_coerce_proc
21
27
 
@@ -49,6 +55,15 @@ module Ably
49
55
  end
50
56
  end
51
57
 
58
+ # Equivalent of {#on} but any exception raised in a block will bubble up and cause this client library to fail.
59
+ # This method should only be used internally by the client library.
60
+ # @api private
61
+ def unsafe_on(*event_names, &block)
62
+ event_names.each do |event_name|
63
+ callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, unsafe: true)
64
+ end
65
+ end
66
+
52
67
  # On receiving an event maching the event_name, call the provided block only once and remove the registered callback
53
68
  #
54
69
  # @param [Array<String>] event_names event name
@@ -60,12 +75,25 @@ module Ably
60
75
  end
61
76
  end
62
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 should only be used internally by the client library.
80
+ # @api private
81
+ def unsafe_once(*event_names, &block)
82
+ event_names.each do |event_name|
83
+ callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, delete_once_run: true, unsafe: true)
84
+ end
85
+ end
86
+
63
87
  # Trigger an event with event_name that will in turn call all matching callbacks setup with `on`
64
88
  def trigger(event_name, *args)
65
89
  callbacks[callbacks_event_coerced(event_name)].
66
90
  clone.
67
91
  select do |proc_hash|
68
- proc_hash[:trigger_proc].call(*args)
92
+ if proc_hash[:unsafe]
93
+ proc_hash[:trigger_proc].call *args
94
+ else
95
+ safe_yield proc_hash[:trigger_proc], *args
96
+ end
69
97
  end.each do |callback|
70
98
  callbacks[callbacks_event_coerced(event_name)].delete callback
71
99
  end
@@ -108,7 +136,8 @@ module Ably
108
136
  block.call *args
109
137
  true if options[:delete_once_run]
110
138
  end,
111
- block: block
139
+ block: block,
140
+ unsafe: options[:unsafe]
112
141
  }
113
142
  end
114
143
 
@@ -0,0 +1,77 @@
1
+ require 'ably/util/pub_sub'
2
+
3
+ module Ably::Modules
4
+ # Message emitter, subscriber and unsubscriber (Pub/Sub) functionality common to Channels and Presence
5
+ # In addition to standard Pub/Sub functionality, it allows subscribers to subscribe to :all.
6
+ module MessageEmitter
7
+ # Subscribe to events on this object
8
+ #
9
+ # @param name [String,Symbol] Optional, the event name to subscribe to. Defaults to `:all` events
10
+ # @yield [Object] For each event, the provided block is called with the event payload object
11
+ #
12
+ # @return [void]
13
+ #
14
+ def subscribe(*names, &callback)
15
+ raise ArgumentError, 'Block required to subscribe to events' unless block_given?
16
+ names = :all unless names && !names.empty?
17
+ Array(names).uniq.each do |name|
18
+ message_emitter_subscriptions[message_emitter_subscriptions_message_name_key(name)] << callback
19
+ end
20
+ end
21
+
22
+ # Unsubscribe the matching block for events on the this object.
23
+ # If a block is not provided, all subscriptions will be unsubscribed
24
+ #
25
+ # @param name [String,Symbol] Optional, the event name to unsubscribe from. Defaults to `:all` events
26
+ #
27
+ # @return [void]
28
+ #
29
+ def unsubscribe(*names, &callback)
30
+ names = :all unless names && !names.empty?
31
+ Array(names).each do |name|
32
+ if name == :all
33
+ message_emitter_subscriptions.keys
34
+ else
35
+ Array(message_emitter_subscriptions_message_name_key(name))
36
+ end.each do |key|
37
+ message_emitter_subscriptions[key].delete_if do |block|
38
+ !block_given? || callback == block
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ # Emit a message to message subscribers
45
+ #
46
+ # param name [String,Symbol] the event name
47
+ # param payload [Object] the event object to emit
48
+ #
49
+ # @return [void]
50
+ #
51
+ # @api private
52
+ def emit_message(name, payload)
53
+ raise 'Event name is required' unless name
54
+
55
+ message_emitter_subscriptions[:all].each { |cb| cb.call(payload) }
56
+ message_emitter_subscriptions[name].each { |cb| cb.call(payload) }
57
+ end
58
+
59
+ private
60
+ def message_emitter_subscriptions
61
+ @message_emitter_subscriptions ||= Hash.new { |hash, key| hash[key] = [] }
62
+ end
63
+
64
+ def message_emitter_subscriptions_message_name_key(name)
65
+ if name == :all
66
+ :all
67
+ else
68
+ message_emitter_subscriptions_coerce_message_key(name)
69
+ end
70
+ end
71
+
72
+ # this method can be overwritten easily to enforce use of set key types§
73
+ def message_emitter_subscriptions_coerce_message_key(name)
74
+ name.to_s
75
+ end
76
+ end
77
+ end