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,111 @@
1
+ require 'statesman'
2
+
3
+ module Ably::Realtime
4
+ class Connection
5
+ module StatesmanMonkeyPatch
6
+ # Override Statesman's #before_transition to support :from arrays
7
+ # This can be removed once https://github.com/gocardless/statesman/issues/95 is solved
8
+ def before_transition(options, &block)
9
+ if options.fetch(:from, nil).kind_of?(Array)
10
+ options[:from].each do |from_state|
11
+ super(options.merge(from: from_state), &block)
12
+ end
13
+ else
14
+ super
15
+ end
16
+ end
17
+ end
18
+
19
+ # Internal class to manage connection state, recovery and state transitions for an {Ably::Realtime::Connection}
20
+ class ConnectionStateMachine
21
+ include Statesman::Machine
22
+ extend StatesmanMonkeyPatch
23
+
24
+ # States supported by this StateMachine match #{Connection::STATE}s
25
+ # :initialized
26
+ # :connecting
27
+ # :connected
28
+ # :disconnected
29
+ # :suspended
30
+ # :closed
31
+ # :failed
32
+ Connection::STATE.each_with_index do |state_enum, index|
33
+ state state_enum.to_sym, initial: index == 0
34
+ end
35
+
36
+ transition :from => :initialized, :to => [:connecting, :closed]
37
+ transition :from => :connecting, :to => [:connected, :failed, :closed]
38
+ transition :from => :connected, :to => [:disconnected, :suspended, :closed, :failed]
39
+ transition :from => :disconnected, :to => [:connecting, :closed]
40
+ transition :from => :suspended, :to => [:connecting, :closed]
41
+ transition :from => :closed, :to => [:connecting]
42
+ transition :from => :failed, :to => [:connecting]
43
+
44
+ before_transition(to: [:connecting], from: [:initialized, :closed, :failed]) do |connection|
45
+ connection.setup_transport do |transport|
46
+ # Transition this StateMachine once the transport is connected or disconnected
47
+ # Invalid state changes are simply ignored and logged
48
+ transport.on(:disconnected) do
49
+ connection.transition_state_machine :disconnected
50
+ end
51
+ end
52
+ end
53
+
54
+ before_transition(to: [:connecting], from: [:disconnected, :suspended]) do |connection|
55
+ connection.reconnect_transport
56
+ end
57
+
58
+ after_transition(to: [:failed]) do |connection|
59
+ connection.transport.disconnect
60
+ end
61
+
62
+ before_transition(to: [:closed], from: [:initialized]) do |connection|
63
+ connection.timers.fetch(:initializer, []).each(&:cancel)
64
+ end
65
+
66
+ before_transition(to: [:closed], from: [:connecting, :connected, :disconnected, :suspended]) do |connection|
67
+ connection.send_protocol_message action: Ably::Models::ProtocolMessage::ACTION.Close
68
+ connection.transport.disconnect
69
+ end
70
+
71
+ after_transition do |connection, transition|
72
+ connection.change_state transition.to_state
73
+ end
74
+
75
+ def initialize(connection)
76
+ @connection = connection
77
+ super(connection)
78
+ end
79
+
80
+ # Override Statesman's #transition_to to simply log state change failures
81
+ def transition_to(*args)
82
+ unless super(*args)
83
+ logger.debug "Unable to transition to #{args[0]} from #{current_state}"
84
+ end
85
+ end
86
+
87
+ private
88
+ attr_reader :connection
89
+
90
+ # TODO: Implement once CLOSED ProtocolMessage is sent back from Ably in response to a CLOSE message
91
+ #
92
+ # FORCE_CONNECTION_CLOSED_TIMEOUT = 5
93
+ #
94
+ # def force_closed_unless_server_acknowledge_closed
95
+ # timeouts[:close_connection] << EventMachine::Timer.new(FORCE_CONNECTION_CLOSED_TIMEOUT) do
96
+ # transition_to :closed
97
+ # end
98
+ # end
99
+ #
100
+ # def clear_force_closed_timeouts
101
+ # timeouts[:close_connection].each do |timeout|
102
+ # timeout.cancel
103
+ # end.clear
104
+ # end
105
+
106
+ def logger
107
+ connection.logger
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,161 @@
1
+ module Ably::Realtime
2
+ class Connection
3
+ # EventMachine WebSocket transport
4
+ # @api private
5
+ class WebsocketTransport < EventMachine::Connection
6
+ include Ably::Modules::EventEmitter
7
+ include Ably::Modules::Conversions
8
+ extend Ably::Modules::Enum
9
+
10
+ # Valid WebSocket connection states
11
+ STATE = ruby_enum('STATE',
12
+ :initialized,
13
+ :connecting,
14
+ :connected,
15
+ :disconnecting,
16
+ :disconnected
17
+ )
18
+ include Ably::Modules::StateEmitter
19
+
20
+ def initialize(connection)
21
+ @connection = connection
22
+ @state = STATE.Initialized
23
+ end
24
+
25
+ # Send object down the WebSocket driver connection as a serialized string/byte array based on protocol
26
+ # @param [Object] object to serialize and send to the WebSocket driver
27
+ # @api public
28
+ def send_object(object)
29
+ case client.protocol
30
+ when :json
31
+ driver.text(object.to_json)
32
+ when :msgpack
33
+ driver.binary(object.to_msgpack.unpack('c*'))
34
+ else
35
+ client.logger.error "Unsupported protocol '#{client.protocol}' for serialization, object cannot be serialized and sent to Ably over this WebSocket"
36
+ end
37
+ end
38
+
39
+ # Disconnect the socket transport connection and write all pending text.
40
+ # If Disconnected state is not automatically triggered, it will be triggered automatically
41
+ # @return <void>
42
+ # @api public
43
+ def disconnect
44
+ close_connection_after_writing
45
+ change_state STATE.Disconnecting
46
+ create_timer(2) do
47
+ # if connection is not disconnected within 2s, set state as disconnected
48
+ change_state STATE.Disconnected
49
+ end
50
+ end
51
+
52
+ # Network connection has been established
53
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
54
+ def post_init
55
+ clear_timer
56
+ change_state STATE.Connecting
57
+ setup_driver
58
+ end
59
+
60
+ # Remote TCP connection attempt completes successfully
61
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
62
+ def connection_completed
63
+ change_state STATE.Connected
64
+ start_tls if client.use_tls?
65
+ driver.start
66
+ end
67
+
68
+ # Called by the event loop whenever data has been received by the network connection.
69
+ # Simply pass onto the WebSocket driver to process and determine content boundaries.
70
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
71
+ def receive_data(data)
72
+ driver.parse(data)
73
+ end
74
+
75
+ # Called whenever a connection (either a server or client connection) is closed
76
+ # Required {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine/Connection EventMachine::Connection} interface
77
+ def unbind
78
+ change_state STATE.Disconnected
79
+ end
80
+
81
+ # URL end point including initialization configuration
82
+ # {http://www.rubydoc.info/gems/websocket-driver/0.3.5/WebSocket/Driver WebSocket::Driver} interface
83
+ def url
84
+ URI(client.endpoint).tap do |endpoint|
85
+ endpoint.query = URI.encode_www_form(client.auth.auth_params.merge(
86
+ timestamp: as_since_epoch(Time.now),
87
+ format: client.protocol,
88
+ echo: client.echo_messages
89
+ ))
90
+ end.to_s
91
+ end
92
+
93
+ # {http://www.rubydoc.info/gems/websocket-driver/0.3.5/WebSocket/Driver WebSocket::Driver} interface
94
+ def write(data)
95
+ send_data(data)
96
+ end
97
+
98
+ # True if socket connection is ready to be released
99
+ # i.e. it is not currently connecting or connected
100
+ def ready_for_release?
101
+ !connecting? && !connected?
102
+ end
103
+
104
+ private
105
+ attr_reader :connection, :driver
106
+
107
+ def clear_timer
108
+ if @timer
109
+ @timer.cancel
110
+ @timer = nil
111
+ end
112
+ end
113
+
114
+ def create_timer(period, &block)
115
+ @timer = EventMachine::Timer.new(period) do
116
+ block.call
117
+ end
118
+ end
119
+
120
+ def setup_driver
121
+ @driver = WebSocket::Driver.client(self)
122
+
123
+ driver.on("open") do
124
+ logger.debug "WebSocket connection opened to #{url}, waiting for Connected protocol message"
125
+ end
126
+
127
+ driver.on("message") do |event|
128
+ event_data = parse_event_data(event.data).freeze
129
+ protocol_message = Ably::Models::ProtocolMessage.new(event_data)
130
+ logger.debug "Prot msg recv <=: #{protocol_message.action} #{event_data}"
131
+ if protocol_message.invalid?
132
+ logger.error "Invalid Protocol Message received: #{event_data}\nNo action taken"
133
+ else
134
+ connection.__incoming_protocol_msgbus__.publish :message, protocol_message
135
+ end
136
+ end
137
+ end
138
+
139
+ def client
140
+ connection.client
141
+ end
142
+
143
+ # Used to log transport messages
144
+ def logger
145
+ connection.logger
146
+ end
147
+
148
+ def parse_event_data(data)
149
+ case client.protocol
150
+ when :json
151
+ JSON.parse(data)
152
+ when :msgpack
153
+ MessagePack.unpack(data.pack('c*'))
154
+ else
155
+ client.logger.error "Unsupported Protocol Message format #{client.protocol}"
156
+ data
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,270 @@
1
+ module Ably::Realtime
2
+ # Presence provides access to presence operations and state for the associated Channel
3
+ class Presence
4
+ include Ably::Modules::EventEmitter
5
+ extend Ably::Modules::Enum
6
+
7
+ STATE = ruby_enum('STATE',
8
+ :initialized,
9
+ :entering,
10
+ :entered,
11
+ :leaving,
12
+ :left,
13
+ :failed
14
+ )
15
+ include Ably::Modules::StateEmitter
16
+
17
+ # {Ably::Realtime::Channel} this Presence object is assoicated with
18
+ attr_reader :channel
19
+
20
+ def initialize(channel)
21
+ @channel = channel
22
+ @state = STATE.Initialized
23
+ @members = Hash.new
24
+ @subscriptions = Hash.new { |hash, key| hash[key] = [] }
25
+ @client_id = client.client_id
26
+ @client_data = nil
27
+
28
+ setup_event_handlers
29
+ end
30
+
31
+ # Enter this client into this channel. This client will be added to the presence set
32
+ # and presence subscribers will see an enter message for this client.
33
+ # @param [Hash,String] options an options Hash to specify client data and/or client ID, or a String with the client data
34
+ # @option options [String] :client_data optional data (eg a status message) for this member
35
+ # @option options [String] :client_id the optional id of the client.
36
+ # This option is provided to support connections from server instances that act on behalf of
37
+ # multiple client_ids. In order to be able to enter the channel with this method, the client
38
+ # library must have been instanced either with a key, or with a token bound to the wildcard clientId.
39
+ # @yield [Ably::Realtime::Presence] On success, will call the block with the {Ably::Realtime::Presence}
40
+ # @return [Ably::Realtime::PresenceMessage] Deferrable {Ably::Realtime::PresenceMessage} that supports both success (callback) and failure (errback) callbacks
41
+ #
42
+ def enter(options = {}, &blk)
43
+ @client_id = options.fetch(:client_id, client_id)
44
+ @client_data = options.fetch(:client_data, client_data)
45
+
46
+ raise Ably::Exceptions::Standard.new('Unable to enter presence channel without a client_id', 400, 91000) unless client_id
47
+
48
+ if state == STATE.Entered
49
+ blk.call self if block_given?
50
+ return
51
+ end
52
+
53
+ ensure_channel_attached do
54
+ once(STATE.Entered) { blk.call self } if block_given?
55
+
56
+ if !entering?
57
+ change_state STATE.Entering
58
+ send_presence_protocol_message(Ably::Models::PresenceMessage::ACTION.Enter).tap do |deferrable|
59
+ deferrable.errback { |message, error| change_state STATE.Failed, error }
60
+ deferrable.callback { |message| change_state STATE.Entered }
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # Leave this client from this channel. This client will be removed from the presence
67
+ # set and presence subscribers will see a leave message for this client.
68
+ # @param (see Presence#enter)
69
+ # @yield (see Presence#enter)
70
+ # @return (see Presence#enter)
71
+ #
72
+ def leave(options = {}, &blk)
73
+ raise Ably::Exceptions::Standard.new('Unable to leave presence channel that is not entered', 400, 91002) unless ably_to_leave?
74
+
75
+ @client_data = options.fetch(:client_data, client_data)
76
+
77
+ if state == STATE.Left
78
+ blk.call self if block_given?
79
+ return
80
+ end
81
+
82
+ ensure_channel_attached do
83
+ once(STATE.Left) { blk.call self } if block_given?
84
+
85
+ if !leaving?
86
+ change_state STATE.Leaving
87
+ send_presence_protocol_message(Ably::Models::PresenceMessage::ACTION.Leave).tap do |deferrable|
88
+ deferrable.errback { |message, error| change_state STATE.Failed, error }
89
+ deferrable.callback { |message| change_state STATE.Left }
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # Update the presence data for this client. If the client is not already a member of
96
+ # the presence set it will be added, and presence subscribers will see an enter or
97
+ # update message for this client.
98
+ # @param (see Presence#enter)
99
+ # @yield (see Presence#enter)
100
+ # @return (see Presence#enter)
101
+ #
102
+ def update(options = {}, &blk)
103
+ @client_data = options.fetch(:client_data, client_data)
104
+
105
+ ensure_channel_attached do
106
+ send_presence_protocol_message(Ably::Models::PresenceMessage::ACTION.Update).tap do |deferrable|
107
+ deferrable.callback do |message|
108
+ change_state STATE.Entered unless entered?
109
+ blk.call self if block_given?
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ # Get the presence state for this Channel.
116
+ # Optionally get a member's {Ably::Models::PresenceMessage} state by member_id
117
+ # @return [Array<Ably::Models::PresenceMessage>, Ably::Models::PresenceMessage] members on the channel
118
+ def get()
119
+ members.map { |key, presence| presence }
120
+ end
121
+
122
+ # Subscribe to presence events on the associated Channel.
123
+ # This implicitly attaches the Channel if it is not already attached.
124
+ #
125
+ # @param action [Ably::Models::PresenceMessage::ACTION] Optional, the state change action to subscribe to. Defaults to all presence actions
126
+ # @yield [Ably::Models::PresenceMessage] For each presence state change event, the block is called
127
+ #
128
+ def subscribe(action = :all, &blk)
129
+ ensure_channel_attached do
130
+ subscriptions[message_action_key(action)] << blk
131
+ end
132
+ end
133
+
134
+ # Unsubscribe the matching block for presence events on the associated Channel.
135
+ # If a block is not provided, all subscriptions will be unsubscribed
136
+ #
137
+ # @param action [Ably::Models::PresenceMessage::ACTION] Optional, the state change action to subscribe to. Defaults to all presence actions
138
+ #
139
+ def unsubscribe(action = :all, &blk)
140
+ if message_action_key(action) == :all
141
+ subscriptions.keys
142
+ else
143
+ Array(message_action_key(action))
144
+ end.each do |key|
145
+ subscriptions[key].delete_if do |block|
146
+ !block_given? || blk == block
147
+ end
148
+ end
149
+ end
150
+
151
+ # @!attribute [r] __incoming_msgbus__
152
+ # @return [Ably::Util::PubSub] Client library internal channel incoming message bus
153
+ # @api private
154
+ def __incoming_msgbus__
155
+ @__incoming_msgbus__ ||= Ably::Util::PubSub.new(
156
+ coerce_into: Proc.new { |event| Ably::Models::ProtocolMessage::ACTION(event) }
157
+ )
158
+ end
159
+
160
+ private
161
+ attr_reader :members, :subscriptions, :client_id, :client_data
162
+
163
+ def ably_to_leave?
164
+ entering? || entered?
165
+ end
166
+
167
+ def setup_event_handlers
168
+ __incoming_msgbus__.subscribe(:presence) do |presence|
169
+ update_members_from_presence_message presence
170
+ subscriptions[:all].each { |cb| cb.call(presence) }
171
+ subscriptions[presence.action].each { |cb| cb.call(presence) }
172
+ end
173
+
174
+ channel.on(Channel::STATE.Detaching) do
175
+ change_state STATE.Leaving
176
+ end
177
+
178
+ channel.on(Channel::STATE.Detached) do
179
+ change_state STATE.Left
180
+ end
181
+
182
+ channel.on(Channel::STATE.Failed) do
183
+ change_state STATE.Failed unless left? || initialized?
184
+ end
185
+ end
186
+
187
+ # @return [Ably::Models::PresenceMessage] presence message is returned allowing callbacks to be added
188
+ def send_presence_protocol_message(presence_action)
189
+ presence_message = create_presence_message(presence_action)
190
+ unless presence_message.client_id
191
+ raise Ably::Exceptions::Standard.new('Unable to enter create presence message without a client_id', 400, 91000)
192
+ end
193
+
194
+ protocol_message = {
195
+ action: Ably::Models::ProtocolMessage::ACTION.Presence,
196
+ channel: channel.name,
197
+ presence: [presence_message]
198
+ }
199
+
200
+ client.connection.send_protocol_message protocol_message
201
+
202
+ presence_message
203
+ end
204
+
205
+ def create_presence_message(action)
206
+ model = {
207
+ action: Ably::Models::PresenceMessage.ACTION(action).to_i,
208
+ clientId: client_id,
209
+ }
210
+ model.merge!(clientData: client_data) if client_data
211
+
212
+ Ably::Models::PresenceMessage.new(model, nil)
213
+ end
214
+
215
+ def update_members_from_presence_message(presence_message)
216
+ unless presence_message.member_id
217
+ new Ably::Exceptions::ProtocolError.new("Protocol error, presence message is missing memberId", 400, 80013)
218
+ end
219
+
220
+ case presence_message.action
221
+ when Ably::Models::PresenceMessage::ACTION.Enter
222
+ members[presence_message.member_id] = presence_message
223
+
224
+ when Ably::Models::PresenceMessage::ACTION.Update
225
+ members[presence_message.member_id] = presence_message
226
+
227
+ when Ably::Models::PresenceMessage::ACTION.Leave
228
+ members.delete presence_message.member_id
229
+
230
+ else
231
+ new Ably::Exceptions::ProtocolError.new("Protocol error, unknown presence action #{presence.action}", 400, 80013)
232
+ end
233
+ end
234
+
235
+ def ensure_channel_attached
236
+ if channel.attached?
237
+ yield
238
+ else
239
+ attach_channel_then { yield }
240
+ end
241
+ end
242
+
243
+ def attach_channel_then
244
+ if channel.detached? || channel.failed?
245
+ raise Ably::Exceptions::Standard.new('Unable to enter presence channel in detached or failed action', 400, 91001)
246
+ else
247
+ channel.once(Channel::STATE.Attached) { yield }
248
+ channel.attach
249
+ end
250
+ end
251
+
252
+ def client
253
+ channel.client
254
+ end
255
+
256
+ # Used by {Ably::Modules::StateEmitter} to debug state changes
257
+ # Used by {Ably::Modules::StateEmitter} to debug action changes
258
+ def logger
259
+ client.logger
260
+ end
261
+
262
+ def message_action_key(action)
263
+ if action == :all
264
+ :all
265
+ else
266
+ Ably::Models::PresenceMessage.ACTION(action)
267
+ end
268
+ end
269
+ end
270
+ end