ably 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -1
- data/ably.gemspec +4 -3
- data/lib/ably.rb +6 -2
- data/lib/ably/auth.rb +24 -16
- data/lib/ably/exceptions.rb +16 -5
- data/lib/ably/{realtime/models → models}/error_info.rb +9 -11
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +57 -26
- data/lib/ably/{realtime/models → models}/message.rb +45 -38
- data/lib/ably/{realtime/models → models}/nil_channel.rb +4 -4
- data/lib/ably/{rest/models/paged_resource.rb → models/paginated_resource.rb} +21 -10
- data/lib/ably/models/presence_message.rb +126 -0
- data/lib/ably/{realtime/models → models}/protocol_message.rb +76 -38
- data/lib/ably/models/token.rb +74 -0
- data/lib/ably/modules/channels_collection.rb +49 -0
- data/lib/ably/modules/conversions.rb +2 -0
- data/lib/ably/modules/event_emitter.rb +43 -8
- data/lib/ably/modules/event_machine_helpers.rb +1 -0
- data/lib/ably/modules/http_helpers.rb +9 -2
- data/lib/ably/modules/message_pack.rb +14 -0
- data/lib/ably/modules/model_common.rb +29 -0
- data/lib/ably/modules/{state.rb → state_emitter.rb} +8 -7
- data/lib/ably/realtime.rb +37 -7
- data/lib/ably/realtime/channel.rb +154 -31
- data/lib/ably/realtime/channels.rb +47 -0
- data/lib/ably/realtime/client.rb +39 -33
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +50 -21
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +9 -11
- data/lib/ably/realtime/connection.rb +148 -79
- data/lib/ably/realtime/connection/connection_state_machine.rb +111 -0
- data/lib/ably/realtime/connection/websocket_transport.rb +161 -0
- data/lib/ably/realtime/presence.rb +270 -0
- data/lib/ably/rest.rb +14 -3
- data/lib/ably/rest/channel.rb +3 -3
- data/lib/ably/rest/channels.rb +26 -12
- data/lib/ably/rest/client.rb +42 -25
- data/lib/ably/rest/middleware/exceptions.rb +21 -23
- data/lib/ably/rest/middleware/external_exceptions.rb +8 -10
- data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +17 -0
- data/lib/ably/rest/middleware/parse_json.rb +9 -2
- data/lib/ably/rest/middleware/parse_message_pack.rb +6 -2
- data/lib/ably/rest/presence.rb +4 -4
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_history_spec.rb +125 -0
- data/spec/acceptance/realtime/channel_spec.rb +135 -63
- data/spec/acceptance/realtime/connection_spec.rb +86 -0
- data/spec/acceptance/realtime/message_spec.rb +116 -94
- data/spec/acceptance/realtime/presence_history_spec.rb +0 -0
- data/spec/acceptance/realtime/presence_spec.rb +277 -0
- data/spec/acceptance/rest/auth_spec.rb +351 -347
- data/spec/acceptance/rest/base_spec.rb +43 -26
- data/spec/acceptance/rest/channel_spec.rb +88 -83
- data/spec/acceptance/rest/channels_spec.rb +32 -28
- data/spec/acceptance/rest/presence_spec.rb +83 -63
- data/spec/acceptance/rest/stats_spec.rb +38 -37
- data/spec/acceptance/rest/time_spec.rb +10 -6
- data/spec/integration/modules/{state_spec.rb → state_emitter_spec.rb} +16 -2
- data/spec/spec_helper.rb +14 -0
- data/spec/support/api_helper.rb +4 -0
- data/spec/support/model_helper.rb +28 -9
- data/spec/support/protocol_msgbus_helper.rb +8 -1
- data/spec/support/test_app.rb +24 -14
- data/spec/unit/{realtime → models}/error_info_spec.rb +4 -4
- data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +46 -9
- data/spec/unit/models/message_spec.rb +229 -0
- data/spec/unit/{rest/paged_resource_spec.rb → models/paginated_resource_spec.rb} +19 -11
- data/spec/unit/models/presence_message_spec.rb +230 -0
- data/spec/unit/models/protocol_message_spec.rb +280 -0
- data/spec/unit/{token_spec.rb → models/token_spec.rb} +18 -22
- data/spec/unit/modules/conversions_spec.rb +1 -1
- data/spec/unit/modules/event_emitter_spec.rb +36 -4
- data/spec/unit/realtime/channel_spec.rb +76 -2
- data/spec/unit/realtime/channels_spec.rb +50 -0
- data/spec/unit/realtime/client_spec.rb +31 -1
- data/spec/unit/realtime/connection_spec.rb +8 -15
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +6 -6
- data/spec/unit/realtime/presence_spec.rb +100 -0
- data/spec/unit/rest/channels_spec.rb +48 -0
- metadata +72 -38
- data/lib/ably/realtime/models/shared.rb +0 -17
- data/lib/ably/rest/models/message.rb +0 -64
- data/lib/ably/rest/models/presence_message.rb +0 -21
- data/lib/ably/token.rb +0 -80
- data/spec/unit/realtime/message_spec.rb +0 -117
- data/spec/unit/realtime/protocol_message_spec.rb +0 -172
- 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
|