ably 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
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