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.
- 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
|