ably 0.1.5 → 0.1.6

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -1
  3. data/ably.gemspec +4 -3
  4. data/lib/ably.rb +6 -2
  5. data/lib/ably/auth.rb +24 -16
  6. data/lib/ably/exceptions.rb +16 -5
  7. data/lib/ably/{realtime/models → models}/error_info.rb +9 -11
  8. data/lib/ably/models/idiomatic_ruby_wrapper.rb +57 -26
  9. data/lib/ably/{realtime/models → models}/message.rb +45 -38
  10. data/lib/ably/{realtime/models → models}/nil_channel.rb +4 -4
  11. data/lib/ably/{rest/models/paged_resource.rb → models/paginated_resource.rb} +21 -10
  12. data/lib/ably/models/presence_message.rb +126 -0
  13. data/lib/ably/{realtime/models → models}/protocol_message.rb +76 -38
  14. data/lib/ably/models/token.rb +74 -0
  15. data/lib/ably/modules/channels_collection.rb +49 -0
  16. data/lib/ably/modules/conversions.rb +2 -0
  17. data/lib/ably/modules/event_emitter.rb +43 -8
  18. data/lib/ably/modules/event_machine_helpers.rb +1 -0
  19. data/lib/ably/modules/http_helpers.rb +9 -2
  20. data/lib/ably/modules/message_pack.rb +14 -0
  21. data/lib/ably/modules/model_common.rb +29 -0
  22. data/lib/ably/modules/{state.rb → state_emitter.rb} +8 -7
  23. data/lib/ably/realtime.rb +37 -7
  24. data/lib/ably/realtime/channel.rb +154 -31
  25. data/lib/ably/realtime/channels.rb +47 -0
  26. data/lib/ably/realtime/client.rb +39 -33
  27. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +50 -21
  28. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +9 -11
  29. data/lib/ably/realtime/connection.rb +148 -79
  30. data/lib/ably/realtime/connection/connection_state_machine.rb +111 -0
  31. data/lib/ably/realtime/connection/websocket_transport.rb +161 -0
  32. data/lib/ably/realtime/presence.rb +270 -0
  33. data/lib/ably/rest.rb +14 -3
  34. data/lib/ably/rest/channel.rb +3 -3
  35. data/lib/ably/rest/channels.rb +26 -12
  36. data/lib/ably/rest/client.rb +42 -25
  37. data/lib/ably/rest/middleware/exceptions.rb +21 -23
  38. data/lib/ably/rest/middleware/external_exceptions.rb +8 -10
  39. data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +17 -0
  40. data/lib/ably/rest/middleware/parse_json.rb +9 -2
  41. data/lib/ably/rest/middleware/parse_message_pack.rb +6 -2
  42. data/lib/ably/rest/presence.rb +4 -4
  43. data/lib/ably/version.rb +1 -1
  44. data/spec/acceptance/realtime/channel_history_spec.rb +125 -0
  45. data/spec/acceptance/realtime/channel_spec.rb +135 -63
  46. data/spec/acceptance/realtime/connection_spec.rb +86 -0
  47. data/spec/acceptance/realtime/message_spec.rb +116 -94
  48. data/spec/acceptance/realtime/presence_history_spec.rb +0 -0
  49. data/spec/acceptance/realtime/presence_spec.rb +277 -0
  50. data/spec/acceptance/rest/auth_spec.rb +351 -347
  51. data/spec/acceptance/rest/base_spec.rb +43 -26
  52. data/spec/acceptance/rest/channel_spec.rb +88 -83
  53. data/spec/acceptance/rest/channels_spec.rb +32 -28
  54. data/spec/acceptance/rest/presence_spec.rb +83 -63
  55. data/spec/acceptance/rest/stats_spec.rb +38 -37
  56. data/spec/acceptance/rest/time_spec.rb +10 -6
  57. data/spec/integration/modules/{state_spec.rb → state_emitter_spec.rb} +16 -2
  58. data/spec/spec_helper.rb +14 -0
  59. data/spec/support/api_helper.rb +4 -0
  60. data/spec/support/model_helper.rb +28 -9
  61. data/spec/support/protocol_msgbus_helper.rb +8 -1
  62. data/spec/support/test_app.rb +24 -14
  63. data/spec/unit/{realtime → models}/error_info_spec.rb +4 -4
  64. data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +46 -9
  65. data/spec/unit/models/message_spec.rb +229 -0
  66. data/spec/unit/{rest/paged_resource_spec.rb → models/paginated_resource_spec.rb} +19 -11
  67. data/spec/unit/models/presence_message_spec.rb +230 -0
  68. data/spec/unit/models/protocol_message_spec.rb +280 -0
  69. data/spec/unit/{token_spec.rb → models/token_spec.rb} +18 -22
  70. data/spec/unit/modules/conversions_spec.rb +1 -1
  71. data/spec/unit/modules/event_emitter_spec.rb +36 -4
  72. data/spec/unit/realtime/channel_spec.rb +76 -2
  73. data/spec/unit/realtime/channels_spec.rb +50 -0
  74. data/spec/unit/realtime/client_spec.rb +31 -1
  75. data/spec/unit/realtime/connection_spec.rb +8 -15
  76. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +6 -6
  77. data/spec/unit/realtime/presence_spec.rb +100 -0
  78. data/spec/unit/rest/channels_spec.rb +48 -0
  79. metadata +72 -38
  80. data/lib/ably/realtime/models/shared.rb +0 -17
  81. data/lib/ably/rest/models/message.rb +0 -64
  82. data/lib/ably/rest/models/presence_message.rb +0 -21
  83. data/lib/ably/token.rb +0 -80
  84. data/spec/unit/realtime/message_spec.rb +0 -117
  85. data/spec/unit/realtime/protocol_message_spec.rb +0 -172
  86. data/spec/unit/rest/message_spec.rb +0 -75
@@ -0,0 +1,74 @@
1
+ module Ably::Models
2
+ # Authentication token issued by Ably in response to an token request
3
+ class Token
4
+ include Ably::Modules::ModelCommon
5
+
6
+ DEFAULTS = {
7
+ capability: { "*" => ["*"] },
8
+ ttl: 60 * 60 # 1 hour
9
+ }
10
+
11
+ # Buffer in seconds given to the use of a token prior to it being considered unusable
12
+ # For example, if buffer is 10s, the token can no longer be used for new requests 9s before it expires
13
+ TOKEN_EXPIRY_BUFFER = 5
14
+
15
+ def initialize(attributes)
16
+ @hash_object = IdiomaticRubyWrapper(attributes.clone.freeze)
17
+ end
18
+
19
+ # @!attribute [r] id
20
+ # @return [String] Unique token ID used to authenticate requests
21
+ def id
22
+ hash.fetch(:id)
23
+ end
24
+
25
+ # @!attribute [r] key_id
26
+ # @return [String] Key ID used to create this token
27
+ def key_id
28
+ hash.fetch(:key)
29
+ end
30
+
31
+ # @!attribute [r] issued_at
32
+ # @return [Time] Time the token was issued
33
+ def issued_at
34
+ as_time_from_epoch(hash.fetch(:issued_at), granularity: :s)
35
+ end
36
+
37
+ # @!attribute [r] expires_at
38
+ # @return [Time] Time the token expires
39
+ def expires_at
40
+ as_time_from_epoch(hash.fetch(:expires), granularity: :s)
41
+ end
42
+
43
+ # @!attribute [r] capability
44
+ # @return [Hash] Capabilities assigned to this token
45
+ def capability
46
+ hash.fetch(:capability)
47
+ end
48
+
49
+ # @!attribute [r] client_id
50
+ # @return [String] Optional client ID assigned to this token
51
+ def client_id
52
+ hash.fetch(:client_id)
53
+ end
54
+
55
+ # @!attribute [r] nonce
56
+ # @return [String] unique nonce used to generate Token and ensure token generation cannot be replayed
57
+ def nonce
58
+ hash.fetch(:nonce)
59
+ end
60
+
61
+ # Returns true if token is expired or about to expire
62
+ #
63
+ # @return [Boolean]
64
+ def expired?
65
+ expires_at < Time.now + TOKEN_EXPIRY_BUFFER
66
+ end
67
+
68
+ # @!attribute [r] hash
69
+ # @return [Hash] Access the token Hash object ruby'fied to use symbolized keys
70
+ def hash
71
+ @hash_object
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,49 @@
1
+ module Ably::Modules
2
+ # ChannelsCollection module provides common functionality to the Rest and Realtime Channels objects
3
+ # such as #get, #[], #fetch, and #release
4
+ module ChannelsCollection
5
+ # Initialize a new Channels object
6
+ #
7
+ # {self} provides simple accessor methods to access a Channel object
8
+ def initialize(client, channel_klass)
9
+ @client = client
10
+ @channel_klass = channel_klass
11
+ @channels = {}
12
+ end
13
+
14
+ # Return a Channel for the given name
15
+ #
16
+ # @param name [String] The name of the channel
17
+ # @param channel_options [Hash] Channel options, currently reserved for Encryption options
18
+ #
19
+ # @return Channel
20
+ def get(name, channel_options = {})
21
+ @channels[name] ||= channel_klass.new(client, name, channel_options)
22
+ end
23
+ alias_method :[], :get
24
+
25
+ # Return a Channel for the given name if it exists, else the block will be called.
26
+ # This method is intentionally similar to {http://ruby-doc.org/core-2.1.3/Hash.html#method-i-fetch Hash#fetch} providing a simple way to check if a channel exists or not without creating one
27
+ #
28
+ # @param name [String] The name of the channel
29
+ #
30
+ # @yield [options] (optional) if a missing_block is passed to this method and no channel exists matching the name, this block is called
31
+ # @yieldparam [String] name of the missing channel
32
+ #
33
+ # @return Channel
34
+ def fetch(name, &missing_block)
35
+ @channels.fetch(name, &missing_block)
36
+ end
37
+
38
+ # Destroy the Channel and releases the associated resources.
39
+ #
40
+ # Releasing a Channel is not typically necessary as a channel consumes no resources other than the memory footprint of the
41
+ # Channel object. Explicitly release channels to free up resources if required
42
+ def release(channel)
43
+ @channels.delete(channel)
44
+ end
45
+
46
+ private
47
+ attr_reader :client, :channel_klass
48
+ end
49
+ end
@@ -1,4 +1,6 @@
1
1
  module Ably::Modules
2
+ # Conversions module provides common timestamp and variable naming conversions to Ably classes.
3
+ # All methods are private
2
4
  module Conversions
3
5
  extend self
4
6
 
@@ -39,24 +39,47 @@ module Ably
39
39
  end
40
40
 
41
41
  # On receiving an event matching the event_name, call the provided block
42
- def on(event_name, &block)
43
- callbacks[callbacks_event_coerced(event_name)] << block
42
+ #
43
+ # @param [Array<String>] event_names event name
44
+ #
45
+ # @return <void>
46
+ def on(*event_names, &block)
47
+ event_names.each do |event_name|
48
+ callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block)
49
+ end
50
+ end
51
+
52
+ # On receiving an event maching the event_name, call the provided block only once and remove the registered callback
53
+ #
54
+ # @param [Array<String>] event_names event name
55
+ #
56
+ # @return <void>
57
+ def once(*event_names, &block)
58
+ event_names.each do |event_name|
59
+ callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, delete_once_run: true)
60
+ end
44
61
  end
45
62
 
46
63
  # Trigger an event with event_name that will in turn call all matching callbacks setup with `on`
47
64
  def trigger(event_name, *args)
48
- callbacks[callbacks_event_coerced(event_name)].each { |cb| cb.call(*args) }
65
+ callbacks[callbacks_event_coerced(event_name)].delete_if { |proc_hash| proc_hash[:trigger_proc].call(*args) }
49
66
  end
50
67
 
51
68
  # Remove all callbacks for event_name.
52
69
  #
53
70
  # If a block is provided, only callbacks matching that block signature will be removed.
54
71
  # If block is not provided, all callbacks matching the event_name will be removed.
55
- def off(event_name, &block)
56
- if block_given?
57
- callbacks[callbacks_event_coerced(event_name)].delete(block)
58
- else
59
- callbacks[callbacks_event_coerced(event_name)].clear
72
+ #
73
+ # @param [Array<String>] event_names event name
74
+ #
75
+ # @return <void>
76
+ def off(*event_names, &block)
77
+ event_names.each do |event_name|
78
+ if block_given?
79
+ callbacks[callbacks_event_coerced(event_name)].delete_if { |proc_hash| proc_hash[:block] == block }
80
+ else
81
+ callbacks[callbacks_event_coerced(event_name)].clear
82
+ end
60
83
  end
61
84
  end
62
85
 
@@ -65,6 +88,18 @@ module Ably
65
88
  klass.extend ClassMethods
66
89
  end
67
90
 
91
+ # Create a Hash with a proc that calls the provided block and returns true if option :delete_once_run is set to true.
92
+ # #trigger automatically deletes any blocks that return true thus allowing a block to be run once
93
+ def proc_for_block(block, options = {})
94
+ {
95
+ trigger_proc: Proc.new do |*args|
96
+ block.call *args
97
+ true if options[:delete_once_run]
98
+ end,
99
+ block: block
100
+ }
101
+ end
102
+
68
103
  def callbacks
69
104
  @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
70
105
  end
@@ -1,4 +1,5 @@
1
1
  module Ably::Modules
2
+ # EventMachineHelpers module provides common private methods to classes simplifying interaction with EventMachine
2
3
  module EventMachineHelpers
3
4
  private
4
5
 
@@ -1,10 +1,12 @@
1
1
  require 'base64'
2
2
 
3
3
  require 'ably/rest/middleware/external_exceptions'
4
+ require 'ably/rest/middleware/fail_if_unsupported_mime_type'
4
5
  require 'ably/rest/middleware/parse_json'
5
6
  require 'ably/rest/middleware/parse_message_pack'
6
7
 
7
8
  module Ably::Modules
9
+ # HttpHelpers provides common private methods to classes to simplify HTTP interactions with Ably
8
10
  module HttpHelpers
9
11
  protected
10
12
  def encode64(text)
@@ -15,11 +17,16 @@ module Ably::Modules
15
17
  "Ably Ruby client #{Ably::VERSION} (https://ably.io)"
16
18
  end
17
19
 
18
- def setup_middleware(builder)
20
+ def setup_outgoing_middleware(builder)
19
21
  # Convert request params to "www-form-urlencoded"
20
22
  builder.use Faraday::Request::UrlEncoded
23
+ end
21
24
 
22
- # Parse JSON / MsgPack response bodies. ParseJson must be first (default) parsing middleware
25
+ def setup_incoming_middleware(builder, options = {})
26
+ # Parse JSON / MsgPack response bodies. ParseJson must be first (default) parsing middleware
27
+ if options[:fail_if_unsupported_mime_type] == true
28
+ builder.use Ably::Rest::Middleware::FailIfUnsupportedMimeType
29
+ end
23
30
  builder.use Ably::Rest::Middleware::ParseJson
24
31
  builder.use Ably::Rest::Middleware::ParseMessagePack
25
32
  end
@@ -0,0 +1,14 @@
1
+ require 'msgpack'
2
+
3
+ module Ably::Modules
4
+ # MessagePack module adds #to_msgpack to the class on the assumption that the class
5
+ # supports the method #as_json
6
+ #
7
+ module MessagePack
8
+ # Generate a packed MsgPack version of this object based on the JSON representation.
9
+ # Keys thus use mixedCase syntax as expected by the Realtime API
10
+ def to_msgpack(*args)
11
+ as_json(*args).to_msgpack
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,29 @@
1
+ module Ably::Modules
2
+ # Common model functionality shared across many {Ably::Models}
3
+ module ModelCommon
4
+ include Ably::Modules::Conversions
5
+ include Ably::Modules::MessagePack
6
+
7
+ # Provide a normal Hash accessor to the underlying raw message object
8
+ #
9
+ # @return [Object]
10
+ def [](key)
11
+ hash[key]
12
+ end
13
+
14
+ def ==(other)
15
+ other.kind_of?(self.class) &&
16
+ hash == other.hash
17
+ end
18
+
19
+ # Return a JSON ready object from the underlying #hash using Ably naming conventions for keys
20
+ def as_json
21
+ hash.as_json.dup
22
+ end
23
+
24
+ # Stringify the JSON representation of this object from the underlying #hash
25
+ def to_json(*args)
26
+ as_json.to_json(*args)
27
+ end
28
+ end
29
+ end
@@ -1,7 +1,7 @@
1
1
  module Ably::Modules
2
- # State module adds a set of generic state related methods to a class on the assumption that
2
+ # StateEmitter module adds a set of generic state related methods to a class on the assumption that
3
3
  # the instance variable @state is used exclusively, the {Enum} STATE is defined prior to inclusion of this
4
- # module, and the class is an {EventEmitter}
4
+ # module, and the class is an {EventEmitter}. It then emits state changes.
5
5
  #
6
6
  # @example
7
7
  # class Connection
@@ -12,7 +12,7 @@ module Ably::Modules
12
12
  # :connecting,
13
13
  # :connected
14
14
  # )
15
- # include Ably::Modules::State
15
+ # include Ably::Modules::StateEmitter
16
16
  # end
17
17
  #
18
18
  # connection = Connection.new
@@ -24,7 +24,7 @@ module Ably::Modules
24
24
  # connection.trigger :invalid # raises an Exception as only a valid state can be used for EventEmitter
25
25
  # connection.change_state :connected # emits :connected event via EventEmitter, returns STATE.Connected
26
26
  #
27
- module State
27
+ module StateEmitter
28
28
  # Current state {Ably::Modules::Enum}
29
29
  #
30
30
  # @return [Symbol] state
@@ -42,11 +42,12 @@ module Ably::Modules
42
42
  # Set the current state {Ably::Modules::Enum}
43
43
  #
44
44
  # @return [Symbol] new state
45
- def state=(new_state)
45
+ # @api private
46
+ def state=(new_state, *args)
46
47
  if state != new_state
47
- logger.debug("#{self.class}: State changed from #{state} => #{new_state}") if respond_to?(:logger, true)
48
+ logger.debug("#{self.class}: StateEmitter changed from #{state} => #{new_state}") if respond_to?(:logger, true)
48
49
  @state = STATE(new_state)
49
- trigger @state
50
+ trigger @state, *args
50
51
  end
51
52
  end
52
53
  alias_method :change_state, :state=
data/lib/ably/realtime.rb CHANGED
@@ -4,22 +4,52 @@ require "websocket/driver"
4
4
  require "ably/modules/event_emitter"
5
5
 
6
6
  require "ably/realtime/channel"
7
+ require "ably/realtime/channels"
7
8
  require "ably/realtime/client"
8
9
  require "ably/realtime/connection"
10
+ require "ably/realtime/connection/connection_state_machine"
11
+ require "ably/realtime/connection/websocket_transport"
12
+ require "ably/realtime/presence"
9
13
 
10
- require "ably/realtime/models/shared"
11
- require "ably/realtime/models/error_info"
12
- require "ably/realtime/models/message"
13
- require "ably/realtime/models/nil_channel"
14
- require "ably/realtime/models/protocol_message"
14
+ Dir.glob(File.expand_path("models/*.rb", File.dirname(__FILE__))).each do |file|
15
+ require file
16
+ end
15
17
 
16
18
  require "ably/realtime/client/incoming_message_dispatcher"
17
19
  require "ably/realtime/client/outgoing_message_dispatcher"
18
20
 
19
21
  module Ably
22
+ # Realtime provides the top-level class to be instanced for the Ably Realtime library
23
+ #
24
+ # @example
25
+ # client = Ably::Realtime.new("xxxxx")
26
+ # channel = client.channel("test")
27
+ # channel.subscribe do |message|
28
+ # message[:name] #=> "greeting"
29
+ # end
30
+ # channel.publish "greeting", "data"
31
+ #
20
32
  module Realtime
21
- def self.new(*args)
22
- Ably::Realtime::Client.new(*args)
33
+ # Convenience method providing an alias to {Ably::Realtime::Client} constructor.
34
+ #
35
+ # @param (see Ably::Realtime::Client#initialize)
36
+ # @option options (see Ably::Realtime::Client#initialize)
37
+ #
38
+ # @yield (see Ably::Realtime::Client#initialize)
39
+ # @yieldparam (see Ably::Realtime::Client#initialize)
40
+ # @yieldreturn (see Ably::Realtime::Client#initialize)
41
+ #
42
+ # @return [Ably::Realtime::Client]
43
+ #
44
+ # @example
45
+ # # create a new client authenticating with basic auth
46
+ # client = Ably::Realtime.new('key.id:secret')
47
+ #
48
+ # # create a new client authenticating with basic auth and a client_id
49
+ # client = Ably::Realtime.new(api_key: 'key.id:secret', client_id: 'john')
50
+ #
51
+ def self.new(options, &auth_block)
52
+ Ably::Realtime::Client.new(options, &auth_block)
23
53
  end
24
54
  end
25
55
  end
@@ -40,16 +40,22 @@ module Ably
40
40
  :detached,
41
41
  :failed
42
42
  )
43
- include Ably::Modules::State
43
+ include Ably::Modules::StateEmitter
44
44
 
45
45
  # Max number of messages to bundle in a single ProtocolMessage
46
46
  MAX_PROTOCOL_MESSAGE_BATCH_SIZE = 50
47
47
 
48
- attr_reader :client, :name
48
+ attr_reader :client, :name, :options
49
49
 
50
- def initialize(client, name)
50
+ # Initialize a new Channel object
51
+ #
52
+ # @param client [Ably::Rest::Client]
53
+ # @param name [String] The name of the channel
54
+ # @param channel_options [Hash] Channel options, currently reserved for future Encryption options
55
+ def initialize(client, name, channel_options = {})
51
56
  @client = client
52
57
  @name = name
58
+ @options = channel_options.clone.freeze
53
59
  @subscriptions = Hash.new { |hash, key| hash[key] = [] }
54
60
  @queue = []
55
61
  @state = STATE.Initialized
@@ -59,54 +65,137 @@ module Ably
59
65
 
60
66
  # Publish a message on the channel
61
67
  #
62
- # @param event [String] The event name of the message
68
+ # @param name [String] The event name of the message
63
69
  # @param data [String,ByteArray] payload for the message
64
- # @yield [Ably::Realtime::Models::Message] On success, will call the block with the {Ably::Realtime::Models::Message}
65
- # @return [Ably::Realtime::Models::Message]
66
- #
67
- def publish(event, data, &callback)
68
- Models::Message.new({
69
- name: event,
70
- data: data,
71
- timestamp: as_since_epoch(Time.now),
72
- client_id: client.client_id
73
- }, nil).tap do |message|
70
+ # @yield [Ably::Models::Message] On success, will call the block with the {Ably::Models::Message}
71
+ # @return [Ably::Models::Message] Deferrable {Ably::Models::Message} that supports both success (callback) and failure (errback) callbacks
72
+ #
73
+ # @example
74
+ # channel.publish('click', 'body')
75
+ #
76
+ # channel.publish('click', 'body') do |message|
77
+ # puts "#{message.name} event received with #{message.data}"
78
+ # end
79
+ #
80
+ # channel.publish('click', 'body').errback do |message, error|
81
+ # puts "#{message.name} was not received, error #{error.message}"
82
+ # end
83
+ #
84
+ def publish(name, data, &callback)
85
+ create_message(name, data).tap do |message|
74
86
  message.callback(&callback) if block_given?
75
87
  queue_message message
76
88
  end
77
89
  end
78
90
 
79
- def subscribe(event = :all, &blk)
80
- event = event.to_s unless event == :all
91
+ # Subscribe to messages matching providing event name, or all messages if event name not provided
92
+ #
93
+ # @param name [String] The event name of the message to subscribe to if provided. Defaults to all events.
94
+ # @yield [Ably::Models::Message] For each message received, the block is called
95
+ #
96
+ def subscribe(name = :all, &blk)
81
97
  attach unless attached? || attaching?
82
- @subscriptions[event] << blk
98
+ subscriptions[message_name_key(name)] << blk
83
99
  end
84
100
 
85
- def attach
86
- unless attached? || attaching?
87
- change_state STATE.Attaching
88
- send_attach_protocol_message
101
+ # Unsubscribe the matching block for messages matching providing event name, or all messages if event name not provided.
102
+ # If a block is not provided, all subscriptions will be unsubscribed
103
+ #
104
+ # @param name [String] The event name of the message to subscribe to if provided. Defaults to all events.
105
+ #
106
+ def unsubscribe(name = :all, &blk)
107
+ if message_name_key(name) == :all
108
+ subscriptions.keys
109
+ else
110
+ Array(message_name_key(name))
111
+ end.each do |key|
112
+ subscriptions[key].delete_if do |block|
113
+ !block_given? || blk == block
114
+ end
89
115
  end
90
116
  end
91
117
 
92
- def __incoming_protocol_msgbus__
93
- @__incoming_protocol_msgbus__ ||= Ably::Util::PubSub.new(
118
+ # Attach to this channel, and call the block if provided when attached.
119
+ # Attaching to a channel is implicit in when a message is published or #subscribe is called, so it is uncommon
120
+ # to need to call attach explicitly.
121
+ #
122
+ # @yield [Ably::Realtime::Channel] Block is called as soon as this channel is in the Attached state
123
+ #
124
+ def attach(&block)
125
+ if attached?
126
+ block.call self if block_given?
127
+ else
128
+ once(STATE.Attached) { block.call self } if block_given?
129
+ if !attaching?
130
+ change_state STATE.Attaching
131
+ send_attach_protocol_message
132
+ end
133
+ end
134
+ end
135
+
136
+ # Detach this channel, and call the block if provided when in a Detached or Failed state
137
+ #
138
+ # @yield [Ably::Realtime::Channel] Block is called as soon as this channel is in the Detached or Failed state
139
+ #
140
+ def detach(&block)
141
+ if detached? || failed?
142
+ block.call self if block_given?
143
+ else
144
+ once(STATE.Detached, STATE.Failed) { block.call self } if block_given?
145
+ if !detaching?
146
+ change_state STATE.Detaching
147
+ send_detach_protocol_message
148
+ end
149
+ end
150
+ end
151
+
152
+ # Presence object for this Channel. This controls this client's
153
+ # presence on the channel and may also be used to obtain presence information
154
+ # and change events for other members of the channel.
155
+ #
156
+ # @return {Ably::Realtime::Presence}
157
+ #
158
+ def presence
159
+ @presence ||= Presence.new(self)
160
+ end
161
+
162
+ # Return the message history of the channel
163
+ #
164
+ # @param (see Ably::Rest::Channel#history)
165
+ # @option options (see Ably::Rest::Channel#history)
166
+ def history(options = {})
167
+ rest_channel.history(options)
168
+ end
169
+
170
+ # @!attribute [r] __incoming_msgbus__
171
+ # @return [Ably::Util::PubSub] Client library internal channel incoming message bus
172
+ # @api private
173
+ def __incoming_msgbus__
174
+ @__incoming_msgbus__ ||= Ably::Util::PubSub.new(
94
175
  coerce_into: Proc.new { |event| Models::ProtocolMessage::ACTION(event) }
95
176
  )
96
177
  end
97
178
 
98
179
  private
99
- attr_reader :queue
180
+ attr_reader :queue, :subscriptions
100
181
 
101
182
  def setup_event_handlers
102
- __incoming_protocol_msgbus__.subscribe(:message) do |message|
103
- @subscriptions[:all].each { |cb| cb.call(message) }
104
- @subscriptions[message.name].each { |cb| cb.call(message) }
183
+ __incoming_msgbus__.subscribe(:message) do |message|
184
+ subscriptions[:all].each { |cb| cb.call(message) }
185
+ subscriptions[message.name].each { |cb| cb.call(message) }
105
186
  end
106
187
 
107
- on(:attached) do
188
+ on(STATE.Attached) do
108
189
  process_queue
109
190
  end
191
+
192
+ connection.on(Connection::STATE.Closed) do
193
+ change_state STATE.Detached
194
+ end
195
+
196
+ connection.on(Connection::STATE.Failed) do
197
+ change_state STATE.Failed unless detached? || initialized?
198
+ end
110
199
  end
111
200
 
112
201
  # Queue message and process queue if channel is attached.
@@ -129,7 +218,7 @@ module Ably
129
218
  def process_queue
130
219
  condition = -> { attached? && messages_in_queue? }
131
220
  non_blocking_loop_while(condition) do
132
- send_messages_within_protocol_message(queue.shift(MAX_PROTOCOL_MESSAGE_BATCH_SIZE))
221
+ send_messages_within_protocol_message queue.shift(MAX_PROTOCOL_MESSAGE_BATCH_SIZE)
133
222
  end
134
223
  end
135
224
 
@@ -142,16 +231,50 @@ module Ably
142
231
  end
143
232
 
144
233
  def send_attach_protocol_message
234
+ send_state_change_protocol_message Models::ProtocolMessage::ACTION.Attach
235
+ end
236
+
237
+ def send_detach_protocol_message
238
+ send_state_change_protocol_message Models::ProtocolMessage::ACTION.Detach
239
+ end
240
+
241
+ def send_state_change_protocol_message(state)
145
242
  client.connection.send_protocol_message(
146
- action: Models::ProtocolMessage::ACTION.Attach.to_i,
243
+ action: state.to_i,
147
244
  channel: name
148
245
  )
149
246
  end
150
247
 
151
- # Used by {Ably::Modules::State} to debug state changes
248
+ def create_message(name, data)
249
+ model = {
250
+ name: name,
251
+ data: data
252
+ }
253
+ model.merge!(clientId: client.client_id) if client.client_id
254
+
255
+ Models::Message.new(model, nil)
256
+ end
257
+
258
+ def rest_channel
259
+ client.rest_client.channel(name)
260
+ end
261
+
262
+ # Used by {Ably::Modules::StateEmitter} to debug state changes
152
263
  def logger
153
264
  client.logger
154
265
  end
266
+
267
+ def connection
268
+ client.connection
269
+ end
270
+
271
+ def message_name_key(name)
272
+ if name == :all
273
+ :all
274
+ else
275
+ name.to_s
276
+ end
277
+ end
155
278
  end
156
279
  end
157
280
  end