ably 0.1.4 → 0.1.5
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/ably.gemspec +1 -0
- data/lib/ably/auth.rb +9 -13
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +27 -39
- data/lib/ably/modules/conversions.rb +31 -10
- data/lib/ably/modules/enum.rb +201 -0
- data/lib/ably/modules/event_emitter.rb +81 -0
- data/lib/ably/modules/event_machine_helpers.rb +21 -0
- data/lib/ably/modules/http_helpers.rb +13 -0
- data/lib/ably/modules/state.rb +67 -0
- data/lib/ably/realtime.rb +6 -1
- data/lib/ably/realtime/channel.rb +117 -56
- data/lib/ably/realtime/client.rb +7 -50
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +116 -0
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +63 -0
- data/lib/ably/realtime/connection.rb +97 -14
- data/lib/ably/realtime/models/error_info.rb +3 -2
- data/lib/ably/realtime/models/message.rb +28 -3
- data/lib/ably/realtime/models/nil_channel.rb +21 -0
- data/lib/ably/realtime/models/protocol_message.rb +35 -27
- data/lib/ably/rest/client.rb +39 -23
- data/lib/ably/rest/middleware/external_exceptions.rb +1 -1
- data/lib/ably/rest/middleware/parse_json.rb +7 -2
- data/lib/ably/rest/middleware/parse_message_pack.rb +23 -0
- data/lib/ably/rest/models/paged_resource.rb +4 -4
- data/lib/ably/util/pub_sub.rb +32 -0
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_spec.rb +1 -0
- data/spec/acceptance/realtime/message_spec.rb +136 -0
- data/spec/acceptance/rest/base_spec.rb +51 -1
- data/spec/acceptance/rest/presence_spec.rb +7 -2
- data/spec/integration/modules/state_spec.rb +66 -0
- data/spec/{unit → integration/rest}/auth.rb +0 -0
- data/spec/support/api_helper.rb +5 -2
- data/spec/support/protocol_msgbus_helper.rb +29 -0
- data/spec/support/test_app.rb +14 -3
- data/spec/unit/{conversions.rb → modules/conversions_spec.rb} +1 -1
- data/spec/unit/modules/enum_spec.rb +263 -0
- data/spec/unit/modules/event_emitter_spec.rb +81 -0
- data/spec/unit/modules/pub_sub_spec.rb +74 -0
- data/spec/unit/realtime/channel_spec.rb +27 -0
- data/spec/unit/realtime/client_spec.rb +8 -0
- data/spec/unit/realtime/connection_spec.rb +40 -0
- data/spec/unit/realtime/error_info_spec.rb +9 -1
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +36 -0
- data/spec/unit/realtime/message_spec.rb +2 -2
- data/spec/unit/realtime/protocol_message_spec.rb +78 -9
- data/spec/unit/rest/{rest_spec.rb → client_spec.rb} +0 -0
- data/spec/unit/rest/message_spec.rb +1 -1
- metadata +51 -9
- data/lib/ably/realtime/callbacks.rb +0 -15
@@ -0,0 +1,63 @@
|
|
1
|
+
module Ably::Realtime
|
2
|
+
class Client
|
3
|
+
# OutgoingMessageDispatcher is a (private) class that is used to deliver
|
4
|
+
# outgoing {Ably::Realtime::Models::ProtocolMessage}s using the {Ably::Realtime::Connection}
|
5
|
+
# when the connection state is capable of delivering messages
|
6
|
+
class OutgoingMessageDispatcher
|
7
|
+
include Ably::Modules::EventMachineHelpers
|
8
|
+
|
9
|
+
ACTION = Models::ProtocolMessage::ACTION
|
10
|
+
|
11
|
+
def initialize(client)
|
12
|
+
@client = client
|
13
|
+
subscribe_to_outgoing_protocol_message_queue
|
14
|
+
setup_event_handlers
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
attr_reader :client
|
19
|
+
|
20
|
+
def connection
|
21
|
+
client.connection
|
22
|
+
end
|
23
|
+
|
24
|
+
def can_send_messages?
|
25
|
+
connection.connected?
|
26
|
+
end
|
27
|
+
|
28
|
+
def messages_in_outgoing_queue?
|
29
|
+
!outgoing_queue.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
def outgoing_queue
|
33
|
+
connection.__outgoing_message_queue__
|
34
|
+
end
|
35
|
+
|
36
|
+
def pending_queue
|
37
|
+
connection.__pending_message_queue__
|
38
|
+
end
|
39
|
+
|
40
|
+
def deliver_queued_protocol_messages
|
41
|
+
condition = -> { can_send_messages? && messages_in_outgoing_queue? }
|
42
|
+
non_blocking_loop_while(condition) do
|
43
|
+
protocol_message = outgoing_queue.shift
|
44
|
+
pending_queue << protocol_message if protocol_message.ack_required?
|
45
|
+
connection.send_text(protocol_message.to_json)
|
46
|
+
client.logger.debug("Prot msg sent =>: #{protocol_message.action} #{protocol_message}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def subscribe_to_outgoing_protocol_message_queue
|
51
|
+
connection.__outgoing_protocol_msgbus__.subscribe(:message) do |*args|
|
52
|
+
deliver_queued_protocol_messages
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def setup_event_handlers
|
57
|
+
connection.on(:connected) do
|
58
|
+
deliver_queued_protocol_messages
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -1,31 +1,85 @@
|
|
1
1
|
module Ably
|
2
2
|
module Realtime
|
3
|
+
# The Connection class represents the connection associated with an Ably Realtime instance.
|
4
|
+
# The Connection object exposes the lifecycle and parameters of the realtime connection.
|
5
|
+
#
|
6
|
+
# Connections will always be in one of the following states:
|
7
|
+
#
|
8
|
+
# initialized: 0
|
9
|
+
# connecting: 1
|
10
|
+
# connected: 2
|
11
|
+
# disconnected: 3
|
12
|
+
# suspended: 4
|
13
|
+
# closed: 5
|
14
|
+
# failed: 6
|
15
|
+
#
|
16
|
+
# Note that the states are available as Enum-like constants:
|
17
|
+
#
|
18
|
+
# Connection::STATE.Initialized
|
19
|
+
# Connection::STATE.Connecting
|
20
|
+
# Connection::STATE.Connected
|
21
|
+
# Connection::STATE.Disconnected
|
22
|
+
# Connection::STATE.Suspended
|
23
|
+
# Connection::STATE.Closed
|
24
|
+
# Connection::STATE.Failed
|
25
|
+
#
|
26
|
+
# @!attribute [r] state
|
27
|
+
# @return {Ably::Realtime::Connection::STATE} connection state
|
28
|
+
# @!attribute [r] __outgoing_message_queue__
|
29
|
+
# @return [Array] An internal queue used to manage unsent outgoing messages. You should never interface with this array directly.
|
30
|
+
# @!attribute [r] __pending_message_queue__
|
31
|
+
# @return [Array] An internal queue used to manage sent messages. You should never interface with this array directly.
|
32
|
+
#
|
3
33
|
class Connection < EventMachine::Connection
|
4
34
|
include Ably::Modules::Conversions
|
5
|
-
include
|
35
|
+
include Ably::Modules::EventEmitter
|
36
|
+
extend Ably::Modules::Enum
|
37
|
+
|
38
|
+
STATE = ruby_enum('STATE',
|
39
|
+
:initializing,
|
40
|
+
:initialized,
|
41
|
+
:connecting,
|
42
|
+
:connected,
|
43
|
+
:disconnected,
|
44
|
+
:suspended,
|
45
|
+
:closed,
|
46
|
+
:failed
|
47
|
+
)
|
48
|
+
include Ably::Modules::State
|
49
|
+
|
50
|
+
attr_reader :__outgoing_message_queue__, :__pending_message_queue__
|
6
51
|
|
7
52
|
def initialize(client)
|
8
|
-
@client
|
9
|
-
@message_serial
|
53
|
+
@client = client
|
54
|
+
@message_serial = 0
|
55
|
+
@__outgoing_message_queue__ = []
|
56
|
+
@__pending_message_queue__ = []
|
57
|
+
@state = STATE.Initializing
|
10
58
|
end
|
11
59
|
|
12
|
-
|
60
|
+
# Required for test /unit/realtime/connection_spec.rb
|
61
|
+
alias_method :orig_send, :send
|
62
|
+
|
63
|
+
# Add protocol message to the outgoing message queue and notify the dispatcher that a message is
|
64
|
+
# ready to be sent
|
65
|
+
def send_protocol_message(protocol_message)
|
13
66
|
add_message_serial_if_ack_required_to(protocol_message) do
|
14
67
|
protocol_message = Models::ProtocolMessage.new(protocol_message)
|
15
|
-
|
16
|
-
|
68
|
+
__outgoing_message_queue__ << protocol_message
|
69
|
+
logger.debug("Prot msg queued =>: #{protocol_message.action} #{protocol_message}")
|
70
|
+
__outgoing_protocol_msgbus__.publish :message, protocol_message
|
17
71
|
end
|
18
72
|
end
|
19
73
|
|
20
74
|
# EventMachine::Connection interface
|
21
75
|
def post_init
|
22
|
-
|
76
|
+
change_state STATE.Initialized
|
23
77
|
|
24
78
|
setup_driver
|
25
79
|
end
|
26
80
|
|
27
81
|
def connection_completed
|
28
|
-
|
82
|
+
change_state STATE.Connecting
|
29
83
|
|
30
84
|
start_tls if client.use_tls?
|
31
85
|
driver.start
|
@@ -36,7 +90,7 @@ module Ably
|
|
36
90
|
end
|
37
91
|
|
38
92
|
def unbind
|
39
|
-
|
93
|
+
change_state STATE.Disconnected
|
40
94
|
end
|
41
95
|
|
42
96
|
# WebSocket::Driver interface
|
@@ -50,9 +104,29 @@ module Ably
|
|
50
104
|
send_data(data)
|
51
105
|
end
|
52
106
|
|
107
|
+
def send_text(text)
|
108
|
+
driver.text(text)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Client library internal outgoing message bus
|
112
|
+
def __outgoing_protocol_msgbus__
|
113
|
+
@__outgoing_protocol_msgbus__ ||= pub_sub_message_bus
|
114
|
+
end
|
115
|
+
|
116
|
+
# Client library internal incoming message bus
|
117
|
+
def __incoming_protocol_msgbus__
|
118
|
+
@__incoming_protocol_msgbus__ ||= pub_sub_message_bus
|
119
|
+
end
|
120
|
+
|
53
121
|
private
|
54
122
|
attr_reader :client, :driver, :message_serial
|
55
123
|
|
124
|
+
def pub_sub_message_bus
|
125
|
+
Ably::Util::PubSub.new(
|
126
|
+
coerce_into: Proc.new { |event| Models::ProtocolMessage::ACTION(event) }
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
56
130
|
def add_message_serial_if_ack_required_to(protocol_message)
|
57
131
|
if Models::ProtocolMessage.ack_required?(protocol_message[:action])
|
58
132
|
add_message_serial_to(protocol_message) { yield }
|
@@ -74,16 +148,25 @@ module Ably
|
|
74
148
|
@driver = WebSocket::Driver.client(self)
|
75
149
|
|
76
150
|
driver.on("open") do
|
77
|
-
|
78
|
-
|
151
|
+
logger.debug("WebSocket connection opened to #{url}")
|
152
|
+
change_state STATE.Connected
|
79
153
|
end
|
80
154
|
|
81
155
|
driver.on("message") do |event|
|
82
|
-
|
83
|
-
|
84
|
-
|
156
|
+
begin
|
157
|
+
message = Models::ProtocolMessage.new(JSON.parse(event.data).freeze)
|
158
|
+
logger.debug("Prot msg recv <=: #{message.action} #{event.data}")
|
159
|
+
__incoming_protocol_msgbus__.publish :message, message
|
160
|
+
rescue KeyError
|
161
|
+
client.logger.error("Unsupported Protocol Message received, unrecognised 'action': #{event.data}\nNo action taken")
|
162
|
+
end
|
85
163
|
end
|
86
164
|
end
|
165
|
+
|
166
|
+
# Used by {Ably::Modules::State} to debug state changes
|
167
|
+
def logger
|
168
|
+
client.logger
|
169
|
+
end
|
87
170
|
end
|
88
171
|
end
|
89
172
|
end
|
@@ -20,11 +20,12 @@ module Ably::Realtime::Models
|
|
20
20
|
@json_object = IdiomaticRubyWrapper(@raw_json_object.clone.freeze)
|
21
21
|
end
|
22
22
|
|
23
|
-
%w( message code
|
23
|
+
%w( message code status_code ).each do |attribute|
|
24
24
|
define_method attribute do
|
25
25
|
json[attribute.to_sym]
|
26
26
|
end
|
27
27
|
end
|
28
|
+
alias_method :status, :status_code
|
28
29
|
|
29
30
|
def json
|
30
31
|
@json_object
|
@@ -32,7 +33,7 @@ module Ably::Realtime::Models
|
|
32
33
|
alias_method :to_json, :json
|
33
34
|
|
34
35
|
def to_s
|
35
|
-
"Error: #{message} (code: #{code},
|
36
|
+
"Error: #{message} (code: #{code}, status_code: #{status_code})"
|
36
37
|
end
|
37
38
|
end
|
38
39
|
end
|
@@ -1,4 +1,15 @@
|
|
1
1
|
module Ably::Realtime::Models
|
2
|
+
def self.Message(message, protocol_message = nil)
|
3
|
+
case message
|
4
|
+
when Ably::Realtime::Models::Message
|
5
|
+
message.tap do
|
6
|
+
message.assign_to_protocol_message protocol_message
|
7
|
+
end
|
8
|
+
else
|
9
|
+
Message.new(message, protocol_message)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
2
13
|
# A class representing an individual message to be sent or received
|
3
14
|
# via the Ably Realtime service.
|
4
15
|
#
|
@@ -20,11 +31,17 @@ module Ably::Realtime::Models
|
|
20
31
|
class Message
|
21
32
|
include Shared
|
22
33
|
include Ably::Modules::Conversions
|
34
|
+
include EventMachine::Deferrable
|
23
35
|
|
24
|
-
|
36
|
+
# {Message} initializer
|
37
|
+
#
|
38
|
+
# @param json_object [Hash] JSON like object with the underlying message details
|
39
|
+
# @param protocol_message [ProtocolMessage] if this message has been published, then it is associated with a {ProtocolMessage}
|
40
|
+
#
|
41
|
+
def initialize(json_object, protocol_message = nil)
|
25
42
|
@protocol_message = protocol_message
|
26
43
|
@raw_json_object = json_object
|
27
|
-
@json_object = IdiomaticRubyWrapper(
|
44
|
+
@json_object = IdiomaticRubyWrapper(json_object.clone.freeze, stop_at: [:data])
|
28
45
|
end
|
29
46
|
|
30
47
|
%w( name client_id ).each do |attribute|
|
@@ -65,8 +82,16 @@ module Ably::Realtime::Models
|
|
65
82
|
to_json_object.to_json
|
66
83
|
end
|
67
84
|
|
85
|
+
def assign_to_protocol_message(protocol_message)
|
86
|
+
@protocol_message = protocol_message
|
87
|
+
end
|
88
|
+
|
68
89
|
private
|
69
|
-
|
90
|
+
|
91
|
+
def protocol_message
|
92
|
+
raise RuntimeError, "Message is not yet published with a ProtocolMessage. ProtocolMessage is nil" if @protocol_message.nil?
|
93
|
+
@protocol_message
|
94
|
+
end
|
70
95
|
|
71
96
|
def protocol_message_index
|
72
97
|
protocol_message.messages.index(self)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Ably::Realtime::Models
|
2
|
+
# Nil object for Channels, this object is only used within the internal API of this client library
|
3
|
+
class NilChannel
|
4
|
+
include Ably::Modules::EventEmitter
|
5
|
+
extend Ably::Modules::Enum
|
6
|
+
STATE = ruby_enum('STATE', Ably::Realtime::Channel::STATE)
|
7
|
+
include Ably::Modules::State
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@state = STATE.Initialized
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
'Nil channel'
|
15
|
+
end
|
16
|
+
|
17
|
+
def __incoming_protocol_msgbus__
|
18
|
+
@__incoming_protocol_msgbus__ ||= Ably::Util::PubSub.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -7,9 +7,7 @@ module Ably::Realtime::Models
|
|
7
7
|
# for further details on the members of a ProtocolMessage
|
8
8
|
#
|
9
9
|
# @!attribute [r] action
|
10
|
-
# @return [
|
11
|
-
# @!attribute [r] action_sym
|
12
|
-
# @return [Symbol] Protocol Message action as a symbol
|
10
|
+
# @return [ACTION] Protocol Message action {Ably::Modules::Enum} from list of {ACTION}. Returns nil if action is unsupported by protocol.
|
13
11
|
# @!attribute [r] count
|
14
12
|
# @return [Integer] The count field is used for ACK and NACK actions. See {http://docs.ably.io/client-lib-development-guide/protocol/#message-acknowledgement message acknowledgement protocol}
|
15
13
|
# @!attribute [r] error_info
|
@@ -35,13 +33,14 @@ module Ably::Realtime::Models
|
|
35
33
|
#
|
36
34
|
class ProtocolMessage
|
37
35
|
include Shared
|
36
|
+
extend Ably::Modules::Enum
|
38
37
|
include Ably::Modules::Conversions
|
39
38
|
|
40
39
|
# Actions which are sent by the Ably Realtime API
|
41
40
|
#
|
42
41
|
# The values correspond to the ints which the API
|
43
42
|
# understands.
|
44
|
-
|
43
|
+
ACTION = ruby_enum('ACTION',
|
45
44
|
heartbeat: 0,
|
46
45
|
ack: 1,
|
47
46
|
nack: 2,
|
@@ -58,23 +57,11 @@ module Ably::Realtime::Models
|
|
58
57
|
detached: 13,
|
59
58
|
presence: 14,
|
60
59
|
message: 15
|
61
|
-
|
62
|
-
|
63
|
-
# Retrieve an action symbol by the integer value
|
64
|
-
def self.action_sym_for(action_int)
|
65
|
-
@actions_index_by_int ||= ACTIONS.invert.freeze
|
66
|
-
@actions_index_by_int[action_int]
|
67
|
-
end
|
68
|
-
|
69
|
-
# Retrive an action integer value from a symbol and raise an exception if invalid
|
70
|
-
def self.action!(action_sym)
|
71
|
-
ACTIONS.fetch(action_sym)
|
72
|
-
end
|
60
|
+
)
|
73
61
|
|
74
62
|
# Indicates this protocol message action will generate an ACK response such as :message or :presence
|
75
63
|
def self.ack_required?(for_action)
|
76
|
-
|
77
|
-
[action!(:presence), action!(:message)].include?(for_action)
|
64
|
+
[ACTION.Presence, ACTION.Message].include?(ACTION(for_action))
|
78
65
|
end
|
79
66
|
|
80
67
|
def initialize(json_object)
|
@@ -82,16 +69,17 @@ module Ably::Realtime::Models
|
|
82
69
|
@json_object = IdiomaticRubyWrapper(@raw_json_object.clone.freeze)
|
83
70
|
end
|
84
71
|
|
85
|
-
%w(
|
86
|
-
channel channel_serial
|
72
|
+
%w( channel channel_serial
|
87
73
|
connection_id connection_serial ).each do |attribute|
|
88
74
|
define_method attribute do
|
89
75
|
json[attribute.to_sym]
|
90
76
|
end
|
91
77
|
end
|
92
78
|
|
93
|
-
def
|
94
|
-
|
79
|
+
def action
|
80
|
+
ACTION(json[:action])
|
81
|
+
rescue KeyError
|
82
|
+
raise KeyError, "Action '#{json[:action]}' is not supported by ProtocolMessage"
|
95
83
|
end
|
96
84
|
|
97
85
|
def error
|
@@ -103,20 +91,36 @@ module Ably::Realtime::Models
|
|
103
91
|
end
|
104
92
|
|
105
93
|
def message_serial
|
106
|
-
json[:msg_serial]
|
94
|
+
Integer(json[:msg_serial])
|
95
|
+
rescue TypeError
|
96
|
+
raise TypeError, "msg_serial '#{json[:msg_serial]}' is invalid, a positive Integer is expected for a ProtocolMessage"
|
97
|
+
end
|
98
|
+
|
99
|
+
def count
|
100
|
+
[1, json[:count].to_i].max
|
101
|
+
end
|
102
|
+
|
103
|
+
def has_message_serial?
|
104
|
+
message_serial && true
|
105
|
+
rescue TypeError
|
106
|
+
false
|
107
107
|
end
|
108
108
|
|
109
109
|
def messages
|
110
110
|
@messages ||=
|
111
111
|
Array(json[:messages]).map do |message|
|
112
|
-
Message
|
112
|
+
Ably::Realtime::Models.Message(message, self)
|
113
113
|
end
|
114
114
|
end
|
115
115
|
|
116
|
+
def add_message(message)
|
117
|
+
messages << message
|
118
|
+
end
|
119
|
+
|
116
120
|
def presence
|
117
121
|
@presence ||=
|
118
122
|
Array(json[:presence]).map do |message|
|
119
|
-
PresenceMessage
|
123
|
+
PresenceMessage(message, self)
|
120
124
|
end
|
121
125
|
end
|
122
126
|
|
@@ -131,8 +135,8 @@ module Ably::Realtime::Models
|
|
131
135
|
end
|
132
136
|
|
133
137
|
def to_json_object
|
134
|
-
raise
|
135
|
-
raise
|
138
|
+
raise TypeError, ":action is missing, cannot generate valid JSON for ProtocolMessage" unless action
|
139
|
+
raise TypeError, ":msg_serial is missing, cannot generate valid JSON for ProtocolMessage" if ack_required? && !has_message_serial?
|
136
140
|
|
137
141
|
json.dup.tap do |json_object|
|
138
142
|
json_object[:messages] = messages.map(&:to_json_object) unless messages.empty?
|
@@ -143,5 +147,9 @@ module Ably::Realtime::Models
|
|
143
147
|
def to_json(*args)
|
144
148
|
to_json_object.to_json
|
145
149
|
end
|
150
|
+
|
151
|
+
def to_s
|
152
|
+
to_json
|
153
|
+
end
|
146
154
|
end
|
147
155
|
end
|